From 8d72cd17009ccfed2abad79e845238ccc0fbf682 Mon Sep 17 00:00:00 2001 From: Patrick Jackson Date: Fri, 24 May 2019 16:04:35 -0400 Subject: [PATCH 1/3] WIP before reducer with any type refactor --- build.gradle | 2 +- gradle/dependencies.gradle | 5 +- lib/build.gradle | 20 ++++- .../kotlin/org/reduxkotlin/Definitions.kt | 2 +- .../org/reduxkotlin/ApplyMiddlewareTest.kt | 80 +++++++++++++++++++ .../kotlin/org/reduxkotlin/CreateStoreSpec.kt | 23 ++++++ 6 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt create mode 100644 lib/src/test/kotlin/org/reduxkotlin/CreateStoreSpec.kt diff --git a/build.gradle b/build.gradle index b1c4425..0da2ee0 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,6 @@ buildscript { dependencies { classpath deps.plugins.kotlin classpath deps.plugins.dokka - classpath deps.plugins.node } } apply from: "$rootDir/gradle/dependencies.gradle" @@ -22,6 +21,7 @@ allprojects { google() jcenter() maven { url "https://oss.sonatype.org/content/repositories/snapshots" } + maven { url "https://dl.bintray.com/spekframework/spek-dev" } } diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 499409b..c3d81f6 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -1,12 +1,13 @@ ext.versions = [ kotlin : '1.3.30', dokka : '0.9.17', + spek : '2.1.0-alpha.0.9+3d5d865', + atrium : '0.8.0' ] + ext.deps = [ plugins: [ - android: 'com.android.tools.build:gradle:3.3.0', kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}", dokka : "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}", - node : "com.moowork.gradle:gradle-node-plugin:${versions.nodePlugin}" ] ] diff --git a/lib/build.gradle b/lib/build.gradle index fe31107..e13d63d 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,3 +1,6 @@ +repositories { + maven { url "https://dl.bintray.com/spekframework/spek-dev" } +} apply plugin: 'java' apply plugin: 'kotlin-multiplatform' @@ -35,10 +38,13 @@ kotlin { } } commonTest { - kotlin.srcDir('src/test') + kotlin.srcDir('src/test/kotlin') dependencies { implementation kotlin("test-common") implementation kotlin("test-annotations-common") + implementation "org.spekframework.spek2:spek-dsl-metadata:$project.versions.spek" + implementation "ch.tutteli.atrium:atrium-cc-en_GB-robstoll-common:$project.versions.atrium" + implementation "io.mockk:mockk-common:1.9.3" } } @@ -52,7 +58,12 @@ kotlin { dependencies { implementation kotlin("test") implementation kotlin("test-junit") - implementation 'junit:junit:4.12' + implementation "org.spekframework.spek2:spek-dsl-jvm:$project.versions.spek" + implementation "ch.tutteli.atrium:atrium-cc-en_GB-robstoll:$project.versions.atrium" + implementation "io.mockk:mockk:1.9.3" + + runtimeOnly "org.spekframework.spek2:spek-runner-junit5:$project.versions.spek" + runtimeOnly 'org.jetbrains.kotlin:kotlin-reflect' } } jsMain { @@ -118,3 +129,8 @@ publishing { } } +jvmTest { + useJUnitPlatform { + includeEngines 'spek2' + } +} diff --git a/lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt b/lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt index 0e4ce81..fb8dc9d 100644 --- a/lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt +++ b/lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt @@ -3,7 +3,7 @@ package org.reduxkotlin /** * see also https://github.com/reactjs/redux/blob/master/docs/Glossary.md#reducer */ -typealias Reducer = (state: ReducerStateType, action: Any) -> ReducerStateType +typealias Reducer = (state: Any, action: Any) -> Any typealias GetState = () -> S typealias StoreSubscriber = ()-> Unit typealias StoreSubscription = () -> Unit diff --git a/lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt b/lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt new file mode 100644 index 0000000..4d78334 --- /dev/null +++ b/lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt @@ -0,0 +1,80 @@ +package org.reduxkotlin + +import ch.tutteli.atrium.api.cc.en_GB.toBe +import ch.tutteli.atrium.api.cc.en_GB.toThrow +import ch.tutteli.atrium.verbs.expect +import io.mockk.spyk +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.specification.describe +import kotlin.test.assertEquals +import kotlin.test.expect + +data class AddTodo(val id: String, val text: String) +data class Todo(val id: String, val text: String, val completed: Boolean = false) + +data class TestState(val todos: List) + +fun todos(state: TestState, action: Any): Any = when (action) { + is AddTodo -> state.copy(todos = state.todos.plus(Todo(action.id, action.text, false))) + else -> state + /* + case 'TOGGLE_TODO': + return state.map(todo => + (todo.id === action.id) + ? {...todo, completed: !todo.completed} + : todo + ) + default: + return state + + */ + } + +object ApplyMiddlewareSpec: Spek( { + describe("middleware") { + it("fails") { + spyk(Any()) + expect(1).toBe(1) + assertEquals(1, 1) + } + } + + describe("applyMiddleware") { + it("warns when dispatching during middleware setup") { + fun dispatchingMiddleware(getState: GetState, next: Dispatcher, action: Any): Any { + next(AddTodo("1", "Dont dispatch in middleware setup")) + return { getState: GetState, next: Dispatcher, action: Any -> next(action)} + } + applyMiddleware(::dispatchingMiddleware)(::createStore)(::todos) + + expect({ + applyMiddleware(dispatchingMiddleware)(createStore)(todos)} + ).toThrow() + }) + + it("wraps dispatch method with middleware once", () => { + fun test(spyOnMethods) { + return methods => { + spyOnMethods(methods) + return next => action => next(action) + } + } + + const spy = jest.fn() + const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos) + + store.dispatch(addTodo("Use Redux")) + store.dispatch(addTodo("Flux FTW!")) + + expect(spy.mock.calls.length).toEqual(1) + + expect(spy.mock.calls[0][0]).toHaveProperty("getState") + expect(spy.mock.calls[0][0]).toHaveProperty("dispatch") + + expect(store.getState()).toEqual([ + { id: 1, text: "Use Redux" }, + { id: 2, text: "Flux FTW!" } + ]) + }) + + }) \ No newline at end of file diff --git a/lib/src/test/kotlin/org/reduxkotlin/CreateStoreSpec.kt b/lib/src/test/kotlin/org/reduxkotlin/CreateStoreSpec.kt new file mode 100644 index 0000000..f443f9d --- /dev/null +++ b/lib/src/test/kotlin/org/reduxkotlin/CreateStoreSpec.kt @@ -0,0 +1,23 @@ +package org.reduxkotlin + +import ch.tutteli.atrium.verbs.expect +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.specification.describe + +object CreateStoreSpec: Spek({ + describe("createStore") { + it("passes the initial state") { + val store = createStore(::todos, TestState( + listOf(Todo( + id = "1", + text = "Hello")) + + expect(store.getState()).toEqual([ + { + id: 1, + text: "Hello" + } + ]) + }) + } +}) \ No newline at end of file From 6c6c78cf030b4a86ac8e12162ca373e554102ceb Mon Sep 17 00:00:00 2001 From: Patrick Jackson Date: Tue, 28 May 2019 10:29:48 -0400 Subject: [PATCH 2/3] remove type from state and create StoreEnhancerWrapper --- .../kotlin/org/reduxkotlin/ApplyMiddleware.kt | 16 +-- .../kotlin/org/reduxkotlin/CreateStore.kt | 8 +- .../kotlin/org/reduxkotlin/Definitions.kt | 50 +++++-- .../org/reduxkotlin/combineEnhancers.kt | 2 +- .../kotlin/org/reduxkotlin/combineReducers.kt | 6 +- .../org/reduxkotlin/ApplyMiddlewareTest.kt | 98 +++++++++----- .../kotlin/org/reduxkotlin/CreateStoreSpec.kt | 125 ++++++++++++++++-- 7 files changed, 231 insertions(+), 74 deletions(-) diff --git a/lib/src/commonMain/kotlin/org/reduxkotlin/ApplyMiddleware.kt b/lib/src/commonMain/kotlin/org/reduxkotlin/ApplyMiddleware.kt index 7c1a170..432b4a9 100644 --- a/lib/src/commonMain/kotlin/org/reduxkotlin/ApplyMiddleware.kt +++ b/lib/src/commonMain/kotlin/org/reduxkotlin/ApplyMiddleware.kt @@ -16,23 +16,21 @@ package org.reduxkotlin * @param {vararg Middleware} [middleware] The middleware chain to be applied. * @returns {StoreEnhancer} A store enhancer applying the middleware. */ -fun applyMiddleware(vararg middlewares: Middleware): StoreEnhancer { +fun applyMiddleware(vararg middlewares: Middleware): StoreEnhancer { return { storeCreator -> - { reducer, initialState -> - val store = storeCreator(reducer, initialState) - //TODO determine if handling dispatching while constructing middleware is needed. - //reduxjs throws an exception if action is dispatched before applymiddleware is complete - /* + { reducer, initialState, en -> + val store = storeCreator(reducer, initialState, en) var dispatch: Dispatcher = { action: Any -> throw Exception( """Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch.""") } - */ + store.dispatch = dispatch + val chain = middlewares.map { middleware -> middleware(store) } + dispatch = compose(chain)(store.dispatch) - val combinedDispatch = middlewares.foldRight(store.dispatch) { middleware, next -> {action -> middleware(store.getState, next, action)}} Store(getState = store.getState, - dispatch = combinedDispatch, + dispatch = dispatch, subscribe = store.subscribe, replaceReducer = store.replaceReducer) } diff --git a/lib/src/commonMain/kotlin/org/reduxkotlin/CreateStore.kt b/lib/src/commonMain/kotlin/org/reduxkotlin/CreateStore.kt index b92550b..516e9f9 100644 --- a/lib/src/commonMain/kotlin/org/reduxkotlin/CreateStore.kt +++ b/lib/src/commonMain/kotlin/org/reduxkotlin/CreateStore.kt @@ -25,11 +25,11 @@ import org.reduxkotlin.utils.isPlainObject * @returns {Store} A Redux store that lets you read the state, dispatch actions * and subscribe to changes. */ -fun createStore(reducer: Reducer, preloadedState: S, enhancer: StoreEnhancer? = null): Store { +fun createStore(reducer: Reducer, preloadedState: Any, enhancer: StoreEnhancer? = null): Store { if (enhancer != null) { - return enhancer { reducer, initialState -> createStore(reducer, initialState) }(reducer, preloadedState) + return enhancer { reducer, initialState, en -> createStore(reducer, initialState) }(reducer, preloadedState, null) } @@ -57,7 +57,7 @@ fun createStore(reducer: Reducer, preloadedState: S, enhancer: StoreEnhan * * @returns {S} The current state tree of your application. */ - fun getState(): S { + fun getState(): Any { if (isDispatching) { throw Exception( """You may not call store.getState() while the reducer is executing. @@ -184,7 +184,7 @@ fun createStore(reducer: Reducer, preloadedState: S, enhancer: StoreEnhan * @param {function} nextReducer The reducer for the store to use instead. * @returns {void} */ - fun replaceReducer(nextReducer: Reducer) { + fun replaceReducer(nextReducer: Reducer) { currentReducer = nextReducer // This action has a similar effect to ActionTypes.INIT. diff --git a/lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt b/lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt index fb8dc9d..7e9318c 100644 --- a/lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt +++ b/lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt @@ -4,27 +4,55 @@ package org.reduxkotlin * see also https://github.com/reactjs/redux/blob/master/docs/Glossary.md#reducer */ typealias Reducer = (state: Any, action: Any) -> Any -typealias GetState = () -> S -typealias StoreSubscriber = ()-> Unit + +typealias GetState = () -> Any +typealias StoreSubscriber = () -> Unit typealias StoreSubscription = () -> Unit -typealias Dispatcher = (Any)->Any -typealias StoreCreator = (reducer: Reducer, initialState: S) -> Store +typealias Dispatcher = (Any) -> Any +typealias StoreCreator = (reducer: Reducer, initialState: Any, s: StoreEnhancerWrapper?) -> Store /** * get a store creator and return a new enhanced one * see https://github.com/reactjs/redux/blob/master/docs/Glossary.md#store-enhancer */ -typealias StoreEnhancer = (next: StoreCreator) -> StoreCreator +typealias StoreEnhancer = (next: StoreCreator) -> StoreCreator + +/** + * wrapper class is needed here to avoid a recursive type declaration. + */ +class StoreEnhancerWrapper(val storeEnhancer2: StoreEnhancer) : StoreEnhancer { + override fun invoke(p1: StoreCreator): StoreCreator { + return storeEnhancer2(p1) + } +} /** * see also https://github.com/reactjs/redux/blob/master/docs/Glossary.md#middleware */ -typealias Middleware = (getState: GetState, nextDispatcher: Dispatcher, action: Any) -> Any +typealias Middleware = (store: Store) -> (next: Dispatcher) -> (action: Any) -> Any -data class Store(val getState: GetState, - var dispatch: Dispatcher, - val subscribe: (StoreSubscriber) -> StoreSubscription, - val replaceReducer: (Reducer) -> Unit) { - val state: S +data class Store( + val getState: GetState, + var dispatch: Dispatcher, + val subscribe: (StoreSubscriber) -> StoreSubscription, + val replaceReducer: (Reducer) -> Unit +) { + val state: Any get() = getState() } + +/** + * Convenience function for creating a middleware + * usage: + * val myMiddleware = middleware { store, dispatch, action -> doStuff() } + */ +fun middleware(dispatch: (Store, dispatch: Dispatcher, action: Any) -> Any): Middleware = + { store -> + { next -> + { action: Any -> + { + dispatch(store, next, action) + } + } + } + } diff --git a/lib/src/commonMain/kotlin/org/reduxkotlin/combineEnhancers.kt b/lib/src/commonMain/kotlin/org/reduxkotlin/combineEnhancers.kt index 2d1049d..abf4b50 100644 --- a/lib/src/commonMain/kotlin/org/reduxkotlin/combineEnhancers.kt +++ b/lib/src/commonMain/kotlin/org/reduxkotlin/combineEnhancers.kt @@ -1,7 +1,7 @@ package org.reduxkotlin -fun combineEnhancers(vararg enhancers: StoreEnhancer): StoreEnhancer = +fun combineEnhancers(vararg enhancers: StoreEnhancer): StoreEnhancer = { storeCreator -> compose(enhancers.map { it })(storeCreator) } diff --git a/lib/src/commonMain/kotlin/org/reduxkotlin/combineReducers.kt b/lib/src/commonMain/kotlin/org/reduxkotlin/combineReducers.kt index ae2ab08..98e7da7 100644 --- a/lib/src/commonMain/kotlin/org/reduxkotlin/combineReducers.kt +++ b/lib/src/commonMain/kotlin/org/reduxkotlin/combineReducers.kt @@ -1,11 +1,11 @@ package org.reduxkotlin -fun combineReducers(vararg reducers: Reducer): Reducer = +fun combineReducers(vararg reducers: Reducer): Reducer = { state, action -> reducers.fold(state, { s, reducer -> reducer(s, action) }) } -fun Reducer.combinedWith(vararg reducers: Reducer): Reducer { +fun Reducer.combinedWith(vararg reducers: Reducer): Reducer { return { state, action -> val sAfterFirstReducer = this(state, action) reducers.fold(sAfterFirstReducer, { s, reducer -> reducer(s, action) }) @@ -15,6 +15,6 @@ fun Reducer.combinedWith(vararg reducers: Reducer): Reducer { /** * combine two reducer with + operator */ -operator fun Reducer.plus(other: Reducer): Reducer = { s, a -> +operator fun Reducer.plus(other: Reducer): Reducer = { s, a -> other(this(s, a), a) } \ No newline at end of file diff --git a/lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt b/lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt index 4d78334..2178cc6 100644 --- a/lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt +++ b/lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt @@ -12,25 +12,47 @@ import kotlin.test.expect data class AddTodo(val id: String, val text: String) data class Todo(val id: String, val text: String, val completed: Boolean = false) -data class TestState(val todos: List) +data class TestState(val todos: List = listOf()) -fun todos(state: TestState, action: Any): Any = when (action) { - is AddTodo -> state.copy(todos = state.todos.plus(Todo(action.id, action.text, false))) - else -> state - /* - case 'TOGGLE_TODO': - return state.map(todo => - (todo.id === action.id) - ? {...todo, completed: !todo.completed} - : todo - ) - default: - return state +fun todos(state: Any, action: Any): Any = + if (state is TestState) { + when (action) { + is AddTodo -> state.copy(todos = state.todos.plus(Todo(action.id, action.text, false))) + else -> state + /* + case 'TOGGLE_TODO': + return state.map(todo => + (todo.id === action.id) + ? {...todo, completed: !todo.completed} + : todo + ) + default: + return state - */ + */ + } + } else { + state + } + +val reduce = castingReducer { state, action -> + when (action) { + + } +} + +fun castingReducer(reducer: ((TestState, Any) -> Any)): Reducer = { state: Any, action: Any -> + if (state is TestState) { + when (action) { + is AddTodo -> state + else -> state + } + } else { + state } +} -object ApplyMiddlewareSpec: Spek( { +object ApplyMiddlewareSpec : Spek({ describe("middleware") { it("fails") { spyk(Any()) @@ -41,32 +63,39 @@ object ApplyMiddlewareSpec: Spek( { describe("applyMiddleware") { it("warns when dispatching during middleware setup") { - fun dispatchingMiddleware(getState: GetState, next: Dispatcher, action: Any): Any { - next(AddTodo("1", "Dont dispatch in middleware setup")) - return { getState: GetState, next: Dispatcher, action: Any -> next(action)} + fun dispatchingMiddleware(store: Store): (next: Dispatcher) -> (action: Any) -> Any { + store.dispatch(AddTodo("1", "Dont dispatch in middleware setup")); + return { next -> + { action -> + { + next(action) + } + } + } } - applyMiddleware(::dispatchingMiddleware)(::createStore)(::todos) - expect({ - applyMiddleware(dispatchingMiddleware)(createStore)(todos)} - ).toThrow() - }) + expect { + val storeEnhancer = applyMiddleware(::dispatchingMiddleware) + storeEnhancer(::createStore)(::todos, Any(), null) + }.toThrow {} + } - it("wraps dispatch method with middleware once", () => { + /* + it("wraps dispatch method with middleware once") { fun test(spyOnMethods) { return methods => { - spyOnMethods(methods) - return next => action => next(action) - } + spyOnMethods(methods) + return next => action => next(action) + } } - const spy = jest.fn() - const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos) + val spy = jest.fn() + val store = applyMiddleware (test(spy), thunk)(createStore)(reducers.todos) - store.dispatch(addTodo("Use Redux")) - store.dispatch(addTodo("Flux FTW!")) + store.dispatch(AddTodo("Use Redux")) + store.dispatch(AddTodo("Flux FTW!")) - expect(spy.mock.calls.length).toEqual(1) + expect(spy.mock.calls.length).toBe(1) expect(spy.mock.calls[0][0]).toHaveProperty("getState") expect(spy.mock.calls[0][0]).toHaveProperty("dispatch") @@ -77,4 +106,7 @@ object ApplyMiddlewareSpec: Spek( { ]) }) - }) \ No newline at end of file + */ + + } +}) \ No newline at end of file diff --git a/lib/src/test/kotlin/org/reduxkotlin/CreateStoreSpec.kt b/lib/src/test/kotlin/org/reduxkotlin/CreateStoreSpec.kt index f443f9d..c8d2c29 100644 --- a/lib/src/test/kotlin/org/reduxkotlin/CreateStoreSpec.kt +++ b/lib/src/test/kotlin/org/reduxkotlin/CreateStoreSpec.kt @@ -1,23 +1,122 @@ package org.reduxkotlin +import ch.tutteli.atrium.api.cc.en_GB.toBe import ch.tutteli.atrium.verbs.expect import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe -object CreateStoreSpec: Spek({ +object CreateStoreSpec : Spek({ describe("createStore") { it("passes the initial state") { - val store = createStore(::todos, TestState( - listOf(Todo( - id = "1", - text = "Hello")) - - expect(store.getState()).toEqual([ - { - id: 1, - text: "Hello" - } - ]) - }) + val store = createStore( + ::todos, TestState( + listOf( + Todo( + id = "1", + text = "Hello" + ) + ) + ) + ) + + expect(store.getState()).toBe( + TestState( + listOf( + Todo( + id = "1", + text = "Hello" + ) + ) + ) + ) + } + it("applies the reducer to the previous state") { + val store = createStore(::todos, TestState()) + expect(store.getState()).toBe(TestState()) + + store.dispatch(Any()) + expect(store.getState()).toBe(TestState()) + + store.dispatch(AddTodo("1", "Hello")) + expect(store.getState()).toBe( + TestState( + listOf( + Todo( + id = "1", + text = "Hello" + ) + ) + ) + ) + + //TODO are ids autoincrement? + store.dispatch(AddTodo("2", "World")) + expect(store.getState()).toBe( + TestState( + listOf( + Todo( + id = "1", + text = "Hello" + ), + Todo( + id = "2", + text = "World" + ) + ) + ) + ) + } + + it("applies the reducer to the initial state") { + val store = createStore( + ::todos, TestState( + listOf( + Todo( + id = "1", + text = "Hello" + ) + ) + ) + ) + expect(store.getState()).toBe( + TestState( + listOf( + Todo( + id = "1", + text = "Hello" + ) + ) + ) + ) + + store.dispatch(Any()) + expect(store.getState()).toBe( + TestState( + listOf( + Todo( + id = "1", + text = "Hello" + ) + ) + ) + ) + + store.dispatch(AddTodo("2", "World")) + expect(store.getState()).toBe( + TestState( + listOf( + Todo( + id = "1", + text = "Hello" + ), + Todo( + id = "2", + text = "World" + ) + ) + ) + ) + } + } }) \ No newline at end of file From 6a42742d88ac5d4c962a125326ce46609143b406 Mon Sep 17 00:00:00 2001 From: Patrick Jackson Date: Wed, 29 May 2019 13:52:12 -0400 Subject: [PATCH 3/3] add castingReducer function --- .../kotlin/org/reduxkotlin/Definitions.kt | 21 ++++++ .../org/reduxkotlin/ApplyMiddlewareTest.kt | 69 +++++++------------ .../kotlin/org/reduxkotlin/CreateStoreSpec.kt | 8 +-- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt b/lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt index 7e9318c..07dd6b6 100644 --- a/lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt +++ b/lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt @@ -1,5 +1,7 @@ package org.reduxkotlin +import kotlin.reflect.KClass + /** * see also https://github.com/reactjs/redux/blob/master/docs/Glossary.md#reducer */ @@ -56,3 +58,22 @@ fun middleware(dispatch: (Store, dispatch: Dispatcher, action: Any) -> Any): Mid } } } + +/** + * Creates a function that returns reducers with state casted to given state. + * This is to assist in readability of creating reducers and remove the need to cast. + * usage: + * * create reducers with castingReducer: + * val reducer = castingReducer { state: MyState, action -> + * when (action) { + * is Todo -> state.copy(...) + * } + * } + */ +inline fun castingReducer(crossinline reducer: ((T, Any) -> Any)): Reducer = { state: Any, action: Any -> + if (T::class.isInstance(state)) { + reducer(state as T, action) + } else { + { state: Any, action: Any -> state } + } +} diff --git a/lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt b/lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt index 2178cc6..10636ca 100644 --- a/lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt +++ b/lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt @@ -7,50 +7,7 @@ import io.mockk.spyk import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe import kotlin.test.assertEquals -import kotlin.test.expect -data class AddTodo(val id: String, val text: String) -data class Todo(val id: String, val text: String, val completed: Boolean = false) - -data class TestState(val todos: List = listOf()) - -fun todos(state: Any, action: Any): Any = - if (state is TestState) { - when (action) { - is AddTodo -> state.copy(todos = state.todos.plus(Todo(action.id, action.text, false))) - else -> state - /* - case 'TOGGLE_TODO': - return state.map(todo => - (todo.id === action.id) - ? {...todo, completed: !todo.completed} - : todo - ) - default: - return state - - */ - } - } else { - state - } - -val reduce = castingReducer { state, action -> - when (action) { - - } -} - -fun castingReducer(reducer: ((TestState, Any) -> Any)): Reducer = { state: Any, action: Any -> - if (state is TestState) { - when (action) { - is AddTodo -> state - else -> state - } - } else { - state - } -} object ApplyMiddlewareSpec : Spek({ describe("middleware") { @@ -76,7 +33,7 @@ object ApplyMiddlewareSpec : Spek({ expect { val storeEnhancer = applyMiddleware(::dispatchingMiddleware) - storeEnhancer(::createStore)(::todos, Any(), null) + storeEnhancer(::createStore)(todos, Any(), null) }.toThrow {} } @@ -109,4 +66,26 @@ object ApplyMiddlewareSpec : Spek({ */ } -}) \ No newline at end of file +}) + +/************** Test Reducer & actions - tobe moved into example app *********/ + +data class AddTodo(val id: String, val text: String) +data class ToggleTodo(val id: String) +data class Todo(val id: String, val text: String, val completed: Boolean = false) + +data class TestState(val todos: List = listOf()) + +val todos = castingReducer { state: TestState, action -> + when (action) { + is AddTodo -> state.copy(todos = state.todos.plus(Todo(action.id, action.text, false))) + is ToggleTodo -> state.copy(todos = state.todos.map { + if (it.id == action.id) { + it.copy(completed = !it.completed) + } else { + it + } + }) + else -> state + } + } \ No newline at end of file diff --git a/lib/src/test/kotlin/org/reduxkotlin/CreateStoreSpec.kt b/lib/src/test/kotlin/org/reduxkotlin/CreateStoreSpec.kt index c8d2c29..98217d7 100644 --- a/lib/src/test/kotlin/org/reduxkotlin/CreateStoreSpec.kt +++ b/lib/src/test/kotlin/org/reduxkotlin/CreateStoreSpec.kt @@ -9,7 +9,7 @@ object CreateStoreSpec : Spek({ describe("createStore") { it("passes the initial state") { val store = createStore( - ::todos, TestState( + todos, TestState( listOf( Todo( id = "1", @@ -31,7 +31,7 @@ object CreateStoreSpec : Spek({ ) } it("applies the reducer to the previous state") { - val store = createStore(::todos, TestState()) + val store = createStore(todos, TestState()) expect(store.getState()).toBe(TestState()) store.dispatch(Any()) @@ -69,7 +69,7 @@ object CreateStoreSpec : Spek({ it("applies the reducer to the initial state") { val store = createStore( - ::todos, TestState( + todos, TestState( listOf( Todo( id = "1", @@ -119,4 +119,4 @@ object CreateStoreSpec : Spek({ } } -}) \ No newline at end of file +})