Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ buildscript {
dependencies {
classpath deps.plugins.kotlin
classpath deps.plugins.dokka
classpath deps.plugins.node
}
}
apply from: "$rootDir/gradle/dependencies.gradle"
Expand All @@ -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" }

}

Expand Down
5 changes: 3 additions & 2 deletions gradle/dependencies.gradle
Original file line number Diff line number Diff line change
@@ -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}"
]
]
20 changes: 18 additions & 2 deletions lib/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
repositories {
maven { url "https://dl.bintray.com/spekframework/spek-dev" }
}
apply plugin: 'java'
apply plugin: 'kotlin-multiplatform'

Expand Down Expand Up @@ -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"
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -118,3 +129,8 @@ publishing {
}
}

jvmTest {
useJUnitPlatform {
includeEngines 'spek2'
}
}
16 changes: 7 additions & 9 deletions lib/src/commonMain/kotlin/org/reduxkotlin/ApplyMiddleware.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <S : Any> applyMiddleware(vararg middlewares: Middleware<S>): StoreEnhancer<S> {
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)
}
Expand Down
8 changes: 4 additions & 4 deletions lib/src/commonMain/kotlin/org/reduxkotlin/CreateStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <S> createStore(reducer: Reducer<S>, preloadedState: S, enhancer: StoreEnhancer<S>? = null): Store<S> {
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)
}


Expand Down Expand Up @@ -57,7 +57,7 @@ fun <S> createStore(reducer: Reducer<S>, 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.
Expand Down Expand Up @@ -184,7 +184,7 @@ fun <S> createStore(reducer: Reducer<S>, preloadedState: S, enhancer: StoreEnhan
* @param {function} nextReducer The reducer for the store to use instead.
* @returns {void}
*/
fun replaceReducer(nextReducer: Reducer<S>) {
fun replaceReducer(nextReducer: Reducer) {
currentReducer = nextReducer

// This action has a similar effect to ActionTypes.INIT.
Expand Down
73 changes: 61 additions & 12 deletions lib/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt
Original file line number Diff line number Diff line change
@@ -1,30 +1,79 @@
package org.reduxkotlin

import kotlin.reflect.KClass

/**
* see also https://github.com/reactjs/redux/blob/master/docs/Glossary.md#reducer
*/
typealias Reducer<ReducerStateType> = (state: ReducerStateType, action: Any) -> ReducerStateType
typealias GetState<S> = () -> S
typealias StoreSubscriber = ()-> Unit
typealias Reducer = (state: Any, action: Any) -> Any

typealias GetState = () -> Any
typealias StoreSubscriber = () -> Unit
typealias StoreSubscription = () -> Unit
typealias Dispatcher = (Any)->Any
typealias StoreCreator<S> = (reducer: Reducer<S>, initialState: S) -> Store<S>
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<S> = (next: StoreCreator<S>) -> StoreCreator<S>
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<State> = (getState: GetState<State>, nextDispatcher: Dispatcher, action: Any) -> Any
typealias Middleware = (store: Store) -> (next: Dispatcher) -> (action: Any) -> Any


data class Store<S>(val getState: GetState<S>,
var dispatch: Dispatcher,
val subscribe: (StoreSubscriber) -> StoreSubscription,
val replaceReducer: (Reducer<S>) -> 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)
}
}
}
}

/**
* 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 <reified T> 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 }
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.reduxkotlin


fun <S> combineEnhancers(vararg enhancers: StoreEnhancer<S>): StoreEnhancer<S> =
fun combineEnhancers(vararg enhancers: StoreEnhancer): StoreEnhancer =
{ storeCreator ->
compose(enhancers.map { it })(storeCreator)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package org.reduxkotlin

fun <S> combineReducers(vararg reducers: Reducer<S>): Reducer<S> =
fun <S> combineReducers(vararg reducers: Reducer): Reducer =
{ state, action ->
reducers.fold(state, { s, reducer -> reducer(s, action) })
}

fun <S> Reducer<S>.combinedWith(vararg reducers: Reducer<S>): Reducer<S> {
fun <S> Reducer.combinedWith(vararg reducers: Reducer): Reducer {
return { state, action ->
val sAfterFirstReducer = this(state, action)
reducers.fold(sAfterFirstReducer, { s, reducer -> reducer(s, action) })
Expand All @@ -15,6 +15,6 @@ fun <S> Reducer<S>.combinedWith(vararg reducers: Reducer<S>): Reducer<S> {
/**
* combine two reducer with + operator
*/
operator fun <S> Reducer<S>.plus(other: Reducer<S>): Reducer<S> = { s, a ->
operator fun <S> Reducer.plus(other: Reducer): Reducer = { s, a ->
other(this(s, a), a)
}
91 changes: 91 additions & 0 deletions lib/src/test/kotlin/org/reduxkotlin/ApplyMiddlewareTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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


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(store: Store): (next: Dispatcher) -> (action: Any) -> Any {
store.dispatch(AddTodo("1", "Dont dispatch in middleware setup"));
return { next ->
{ action ->
{
next(action)
}
}
}
}

expect {
val storeEnhancer = applyMiddleware(::dispatchingMiddleware)
storeEnhancer(::createStore)(todos, Any(), null)
}.toThrow<Exception> {}
}

/*
it("wraps dispatch method with middleware once") {
fun test(spyOnMethods) {
return methods => {
spyOnMethods(methods)
return next => action => next(action)
}
}

val spy = jest.fn()
val store = applyMiddleware (test(spy), thunk)(createStore)(reducers.todos)

store.dispatch(AddTodo("Use Redux"))
store.dispatch(AddTodo("Flux FTW!"))

expect(spy.mock.calls.length).toBe(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!" }
])
})

*/

}
})

/************** 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<Todo> = 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
}
}
Loading