diff --git a/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/NoOpReducer.kt b/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/NoOpReducer.kt index 2957dcc6..f84b1c2e 100644 --- a/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/NoOpReducer.kt +++ b/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/NoOpReducer.kt @@ -3,7 +3,8 @@ package money.vivid.elmslie.core.store /** * Reducer that doesn't change state, and doesn't emit commands or effects */ -class NoOpReducer : StateReducer { +class NoOpReducer : + StateReducer() { - override fun reduce(event: Event, state: State) = Result(state) + override fun Result.reduce(event: Event) = Unit } diff --git a/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/ScreenReducer.kt b/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/ScreenReducer.kt new file mode 100644 index 00000000..ddb7efd5 --- /dev/null +++ b/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/ScreenReducer.kt @@ -0,0 +1,23 @@ +package money.vivid.elmslie.core.store + +import kotlin.reflect.KClass + +abstract class ScreenReducer( + private val uiEventClass: KClass, + private val internalEventClass: KClass +) : StateReducer() { + + + protected abstract fun Result.ui(event: Ui): Any? + + protected abstract fun Result.internal(event: Internal): Any? + + override fun Result.reduce(event: Event) { + @Suppress("UNCHECKED_CAST") + when { + uiEventClass.isInstance(event) -> ui(event as Ui) + internalEventClass.isInstance(event) -> internal(event as Internal) + else -> error("Event ${event::class} is neither UI nor Internal") + } + } +} diff --git a/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/StateReducer.kt b/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/StateReducer.kt index 8bc43319..9ee2f85d 100644 --- a/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/StateReducer.kt +++ b/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/StateReducer.kt @@ -1,6 +1,13 @@ package money.vivid.elmslie.core.store -fun interface StateReducer { +import money.vivid.elmslie.core.store.dsl.ResultBuilder - fun reduce(event: Event, state: State): Result +abstract class StateReducer { + + // Needed to type less code + protected inner class Result(state: State) : ResultBuilder(state) + + protected abstract fun Result.reduce(event: Event) + + fun reduce(event: Event, state: State) = Result(state).apply { reduce(event) }.build() } diff --git a/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/dsl/DslReducer.kt b/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/dsl/DslReducer.kt deleted file mode 100644 index 2d67d09c..00000000 --- a/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/dsl/DslReducer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package money.vivid.elmslie.core.store.dsl - -import money.vivid.elmslie.core.store.StateReducer - -abstract class DslReducer : - StateReducer { - - // Needed to type less code - protected inner class Result(state: State) : ResultBuilder(state) - - protected abstract fun Result.reduce(event: Event): Any? - - final override fun reduce(event: Event, state: State) = Result(state).apply { reduce(event) }.build() -} diff --git a/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/dsl/ScreenDslReducer.kt b/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/dsl/ScreenDslReducer.kt deleted file mode 100644 index e50f073c..00000000 --- a/elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/dsl/ScreenDslReducer.kt +++ /dev/null @@ -1,31 +0,0 @@ -package money.vivid.elmslie.core.store.dsl - -import money.vivid.elmslie.core.store.Result -import money.vivid.elmslie.core.store.StateReducer -import kotlin.reflect.KClass - -abstract class ScreenDslReducer( - private val uiEventClass: KClass, - private val internalEventClass: KClass -) : StateReducer { - - protected inner class Result(state: State) : ResultBuilder(state) - - protected abstract fun Result.ui(event: Ui): Any? - - protected abstract fun Result.internal(event: Internal): Any? - - final override fun reduce( - event: Event, - state: State - ): money.vivid.elmslie.core.store.Result { - val body = Result(state) - @Suppress("UNCHECKED_CAST") - when { - uiEventClass.isInstance(event) -> body.ui(event as Ui) - internalEventClass.isInstance(event) -> body.internal(event as Internal) - else -> error("Event ${event::class} is neither UI nor Internal") - } - return body.build() - } -} diff --git a/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/EffectCachingElmStoreTest.kt b/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/EffectCachingElmStoreTest.kt index d433e218..93446022 100644 --- a/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/EffectCachingElmStoreTest.kt +++ b/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/EffectCachingElmStoreTest.kt @@ -40,14 +40,13 @@ class EffectCachingElmStoreTest { fun `Should collect effects which are emitted before collecting flow`() = runTest { val store = store( - state = State(), - reducer = { event, state -> - Result( - state = state, - effect = Effect(event.value), - ) - }, - ) + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + effects { +Effect(value = event.value) } + } + }, + ) .toCachedStore() store.start() @@ -76,14 +75,13 @@ class EffectCachingElmStoreTest { fun `Should collect effects which are emitted before collecting flow and after`() = runTest { val store = store( - state = State(), - reducer = { event, state -> - Result( - state = state, - effect = Effect(event.value), - ) - }, - ) + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + effects { +Effect(value = event.value) } + } + }, + ) .toCachedStore() store.start() @@ -114,14 +112,13 @@ class EffectCachingElmStoreTest { fun `Should emit effects from cache only for the first subscriber`() = runTest { val store = store( - state = State(), - reducer = { event, state -> - Result( - state = state, - effect = Effect(event.value), - ) - }, - ) + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + effects { +Effect(value = event.value) } + } + }, + ) .toCachedStore() store.start() @@ -152,14 +149,13 @@ class EffectCachingElmStoreTest { fun `Should cache effects if there is no left collectors`() = runTest { val store = store( - state = State(), - reducer = { event, state -> - Result( - state = state, - effect = Effect(event.value), - ) - }, - ) + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + effects { +Effect(value = event.value) } + } + }, + ) .toCachedStore() store.start() diff --git a/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/ElmStoreTest.kt b/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/ElmStoreTest.kt index b0c87fee..7b0098a5 100644 --- a/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/ElmStoreTest.kt +++ b/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/ElmStoreTest.kt @@ -57,15 +57,18 @@ class ElmStoreTest { override fun execute(command: Command): Flow = flow { emit(Event()) }.onEach { delay(1000) } } + val store = store( - state = State(), - reducer = { _, state -> - Result(state = state.copy(value = state.value + 1), command = Command()) - }, - actor = actor, - ) - .start() + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + state { copy(value = state.value + 1) } + commands { +Command() } + } + }, + actor = actor, + ).start() val emittedStates = mutableListOf() val collectJob = launch { store.states.toList(emittedStates) } @@ -91,9 +94,13 @@ class ElmStoreTest { fun `Should update state when event is received`() = runTest { val store = store( - state = State(), - reducer = { event, state -> Result(state = state.copy(value = event.value)) }, - ) + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + state { copy(value = event.value) } + } + }, + ) .start() assertEquals( @@ -110,9 +117,13 @@ class ElmStoreTest { fun `Should not update state when it's equal to previous one`() = runTest { val store = store( - state = State(), - reducer = { event, state -> Result(state = state.copy(value = event.value)) }, - ) + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + state { copy(value = event.value) } + } + }, + ) .start() val emittedStates = mutableListOf() @@ -134,11 +145,13 @@ class ElmStoreTest { fun `Should collect all emitted effects`() = runTest { val store = store( - state = State(), - reducer = { event, state -> - Result(state = state, effect = Effect(value = event.value)) + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + effects { +Effect(value = event.value) } } - ) + }, + ) .start() val effects = mutableListOf() @@ -161,11 +174,13 @@ class ElmStoreTest { fun `Should skip the effect which is emitted before subscribing to effects`() = runTest { val store = store( - state = State(), - reducer = { event, state -> - Result(state = state, effect = Effect(value = event.value)) + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + effects { +Effect(value = event.value) } } - ) + }, + ) .start() val effects = mutableListOf() @@ -188,19 +203,16 @@ class ElmStoreTest { fun `Should collect all effects emitted once per time`() = runTest { val store = store( - state = State(), - reducer = { event, state -> - Result( - state = state, - commands = emptyList(), - effects = - listOf( - Effect(value = event.value), - Effect(value = event.value), - ), - ) + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + effects { + +Effect(value = event.value) + +Effect(value = event.value) + } } - ) + }, + ) .start() val effects = mutableListOf() @@ -222,11 +234,13 @@ class ElmStoreTest { fun `Should collect all emitted effects by all collectors`() = runTest { val store = store( - state = State(), - reducer = { event, state -> - Result(state = state, effect = Effect(value = event.value)) + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + effects { +Effect(value = event.value) } } - ) + }, + ) .start() val effects1 = mutableListOf() @@ -259,11 +273,13 @@ class ElmStoreTest { fun `Should collect duplicated effects`() = runTest { val store = store( - state = State(), - reducer = { event, state -> - Result(state = state, effect = Effect(value = event.value)) + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + effects { +Effect(value = event.value) } } - ) + }, + ) .start() val effects = mutableListOf() @@ -289,15 +305,17 @@ class ElmStoreTest { } val store = store( - state = State(), - reducer = { event, state -> - Result( - state = state.copy(value = event.value), - command = Command(event.value - 1).takeIf { event.value > 0 } - ) - }, - actor = actor, - ) + state = State(), + reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + state { copy(value = event.value) } + commands { + +Command(event.value - 1).takeIf { event.value > 0 } + } + } + }, + actor = actor, + ) .start() val states = mutableListOf() diff --git a/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/dsl/DslReducerTest.kt b/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/dsl/DslReducerTest.kt index 82c25634..3fd9f4da 100644 --- a/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/dsl/DslReducerTest.kt +++ b/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/dsl/DslReducerTest.kt @@ -1,10 +1,11 @@ package money.vivid.elmslie.core.store.dsl +import money.vivid.elmslie.core.store.StateReducer import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -private object BasicDslReducer : DslReducer() { +private object BasicDslReducer : StateReducer() { override fun Result.reduce(event: TestEvent) = when (event) { diff --git a/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/dsl/ScreenDslReducerTest.kt b/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/dsl/ScreenReducerTest.kt similarity index 76% rename from elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/dsl/ScreenDslReducerTest.kt rename to elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/dsl/ScreenReducerTest.kt index 14fbf9f5..ea80efde 100644 --- a/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/dsl/ScreenDslReducerTest.kt +++ b/elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/dsl/ScreenReducerTest.kt @@ -1,18 +1,20 @@ package money.vivid.elmslie.core.store.dsl +import money.vivid.elmslie.core.store.ScreenReducer +import money.vivid.elmslie.core.store.StateReducer import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -object BasicScreenDslReducer : - ScreenDslReducer< - TestScreenEvent, - TestScreenEvent.Ui, - TestScreenEvent.Internal, - TestState, - TestEffect, - TestCommand - >(TestScreenEvent.Ui::class, TestScreenEvent.Internal::class) { +object BasicScreenReducer : + ScreenReducer< + TestScreenEvent, + TestScreenEvent.Ui, + TestScreenEvent.Internal, + TestState, + TestEffect, + TestCommand + >(TestScreenEvent.Ui::class, TestScreenEvent.Internal::class) { override fun Result.ui(event: TestScreenEvent.Ui) = when (event) { @@ -30,7 +32,7 @@ object BasicScreenDslReducer : } // The same code -object PlainScreenDslReducer : DslReducer() { +object PlainScreenDslReducer : StateReducer() { override fun Result.reduce(event: TestScreenEvent) = when (event) { @@ -53,9 +55,9 @@ object PlainScreenDslReducer : DslReducer() { +internal object TimerReducer : StateReducer() { override fun Result.reduce(event: Event) = when (event) { diff --git a/samples/kotlin-calculator/src/main/kotlin/money/vivid/elmslie/samples/calculator/Models.kt b/samples/kotlin-calculator/src/main/kotlin/money/vivid/elmslie/samples/calculator/Models.kt index 293466d8..0d02bc6c 100644 --- a/samples/kotlin-calculator/src/main/kotlin/money/vivid/elmslie/samples/calculator/Models.kt +++ b/samples/kotlin-calculator/src/main/kotlin/money/vivid/elmslie/samples/calculator/Models.kt @@ -17,6 +17,8 @@ data class State( val input: Int = 0 ) +sealed interface Command + enum class Operation( op: (Int, Int) -> Int ) : (Int, Int) -> Int by op { diff --git a/samples/kotlin-calculator/src/main/kotlin/money/vivid/elmslie/samples/calculator/Store.kt b/samples/kotlin-calculator/src/main/kotlin/money/vivid/elmslie/samples/calculator/Store.kt index 47000a5d..95d903d8 100644 --- a/samples/kotlin-calculator/src/main/kotlin/money/vivid/elmslie/samples/calculator/Store.kt +++ b/samples/kotlin-calculator/src/main/kotlin/money/vivid/elmslie/samples/calculator/Store.kt @@ -3,35 +3,35 @@ package money.vivid.elmslie.samples.calculator import money.vivid.elmslie.core.store.ElmStore import money.vivid.elmslie.core.store.NoOpActor import money.vivid.elmslie.core.store.StateReducer -import money.vivid.elmslie.core.store.Result - private const val MAX_INPUT_LENGTH = 9 -val Reducer = StateReducer { event: Event, state: State -> - when (event) { - is Event.EnterDigit -> when { - state.input.toString().length == MAX_INPUT_LENGTH -> { - Result(state, effect = Effect.NotifyError("Reached max input length")) +val Reducer = object : StateReducer() { + override fun Result.reduce(event: Event) { + when (event) { + is Event.EnterDigit -> when { + state.input.toString().length == MAX_INPUT_LENGTH -> { + effects { +Effect.NotifyError("Reached max input length") } + } + + event.digit.isDigit() -> { + state { copy(input = "${state.input}${event.digit}".toInt()) } + } + + else -> effects { +Effect.NotifyError("${event.digit} is not a digit") } + } + + is Event.PerformOperation -> { + val total = state.pendingOperation?.invoke(state.total, state.input) ?: state.total + state { copy(pendingOperation = event.operation, total = total, input = 0) } + effects { +Effect.NotifyNewResult(total) } } - event.digit.isDigit() -> { - Result(state.copy(input = "${state.input}${event.digit}".toInt())) + + is Event.Evaluate -> { + val total = state.pendingOperation?.invoke(state.total, state.input) ?: state.total + state { copy(pendingOperation = null, total = total, input = 0) } + effects { +Effect.NotifyNewResult(total) } } - else -> Result(state, effect = Effect.NotifyError("${event.digit} is not a digit")) - } - is Event.PerformOperation -> { - val total = state.pendingOperation?.invoke(state.total, state.input) ?: state.total - Result( - state = state.copy(pendingOperation = event.operation, total = total, input = 0), - effect = Effect.NotifyNewResult(total) - ) - } - is Event.Evaluate -> { - val total = state.pendingOperation?.invoke(state.total, state.input) ?: state.total - Result( - state = state.copy(pendingOperation = null, total = total, input = 0), - effect = Effect.NotifyNewResult(total) - ) } } }