From 4553dcd7bfbb4abae71746453aeace98d267b44e Mon Sep 17 00:00:00 2001 From: Dmitriy Berdnikov <d.k.berdnikov@tinkoff.ru> Date: Mon, 19 Feb 2024 12:48:42 +0300 Subject: [PATCH] leave only Dsl way to write state reducer --- .../vivid/elmslie/core/store/NoOpReducer.kt | 5 +- .../vivid/elmslie/core/store/ScreenReducer.kt | 23 ++++ .../vivid/elmslie/core/store/StateReducer.kt | 11 +- .../elmslie/core/store/dsl/DslReducer.kt | 14 --- .../core/store/dsl/ScreenDslReducer.kt | 31 ----- .../core/store/EffectCachingElmStoreTest.kt | 60 +++++---- .../vivid/elmslie/core/store/ElmStoreTest.kt | 118 ++++++++++-------- .../elmslie/core/store/dsl/DslReducerTest.kt | 3 +- ...DslReducerTest.kt => ScreenReducerTest.kt} | 26 ++-- .../coroutines/timer/elm/TimerReducer.kt | 4 +- .../elmslie/samples/calculator/Models.kt | 2 + .../vivid/elmslie/samples/calculator/Store.kt | 48 +++---- 12 files changed, 175 insertions(+), 170 deletions(-) create mode 100644 elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/ScreenReducer.kt delete mode 100644 elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/dsl/DslReducer.kt delete mode 100644 elmslie-core/src/main/kotlin/money/vivid/elmslie/core/store/dsl/ScreenDslReducer.kt rename elmslie-core/src/test/kotlin/money/vivid/elmslie/core/store/dsl/{ScreenDslReducerTest.kt => ScreenReducerTest.kt} (76%) 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<Event : Any, State : Any, Effect : Any, Command : Any> : StateReducer<Event, State, Effect, Command> { +class NoOpReducer<Event : Any, State : Any, Effect : Any, Command : Any> : + StateReducer<Event, State, Effect, Command>() { - override fun reduce(event: Event, state: State) = Result<State, Effect, Command>(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<Event : Any, Ui : Any, Internal : Any, State : Any, Effect : Any, Command : Any>( + private val uiEventClass: KClass<Ui>, + private val internalEventClass: KClass<Internal> +) : StateReducer<Event, State, Effect, Command>() { + + + 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<Event : Any, State : Any, Effect : Any, Command : Any> { +import money.vivid.elmslie.core.store.dsl.ResultBuilder - fun reduce(event: Event, state: State): Result<State, Effect, Command> +abstract class StateReducer<Event : Any, State : Any, Effect : Any, Command : Any> { + + // Needed to type less code + protected inner class Result(state: State) : ResultBuilder<State, Effect, Command>(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<Event : Any, State : Any, Effect : Any, Command : Any> : - StateReducer<Event, State, Effect, Command> { - - // Needed to type less code - protected inner class Result(state: State) : ResultBuilder<State, Effect, Command>(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<Event : Any, Ui : Any, Internal : Any, State : Any, Effect : Any, Command : Any>( - private val uiEventClass: KClass<Ui>, - private val internalEventClass: KClass<Internal> -) : StateReducer<Event, State, Effect, Command> { - - protected inner class Result(state: State) : ResultBuilder<State, Effect, Command>(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<State, Effect, Command> { - 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<Event, State, Effect, Command>() { + 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<Event, State, Effect, Command>() { + 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<Event, State, Effect, Command>() { + 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<Event, State, Effect, Command>() { + 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<Event> = 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<Event, State, Effect, Command>() { + override fun Result.reduce(event: Event) { + state { copy(value = state.value + 1) } + commands { +Command() } + } + }, + actor = actor, + ).start() val emittedStates = mutableListOf<State>() 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<Event, State, Effect, Command>() { + 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<Event, State, Effect, Command>() { + override fun Result.reduce(event: Event) { + state { copy(value = event.value) } + } + }, + ) .start() val emittedStates = mutableListOf<State>() @@ -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<Event, State, Effect, Command>() { + override fun Result.reduce(event: Event) { + effects { +Effect(value = event.value) } } - ) + }, + ) .start() val effects = mutableListOf<Effect>() @@ -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<Event, State, Effect, Command>() { + override fun Result.reduce(event: Event) { + effects { +Effect(value = event.value) } } - ) + }, + ) .start() val effects = mutableListOf<Effect>() @@ -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<Event, State, Effect, Command>() { + override fun Result.reduce(event: Event) { + effects { + +Effect(value = event.value) + +Effect(value = event.value) + } } - ) + }, + ) .start() val effects = mutableListOf<Effect>() @@ -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<Event, State, Effect, Command>() { + override fun Result.reduce(event: Event) { + effects { +Effect(value = event.value) } } - ) + }, + ) .start() val effects1 = mutableListOf<Effect>() @@ -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<Event, State, Effect, Command>() { + override fun Result.reduce(event: Event) { + effects { +Effect(value = event.value) } } - ) + }, + ) .start() val effects = mutableListOf<Effect>() @@ -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<Event, State, Effect, Command>() { + 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<State>() 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<TestEvent, TestState, TestEffect, TestCommand>() { +private object BasicDslReducer : StateReducer<TestEvent, TestState, TestEffect, TestCommand>() { 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<TestScreenEvent, TestState, TestEffect, TestCommand>() { +object PlainScreenDslReducer : StateReducer<TestScreenEvent, TestState, TestEffect, TestCommand>() { override fun Result.reduce(event: TestScreenEvent) = when (event) { @@ -53,9 +55,9 @@ object PlainScreenDslReducer : DslReducer<TestScreenEvent, TestState, TestEffect } } -internal class ScreenDslReducerTest { +internal class ScreenReducerTest { - private val reducer = BasicScreenDslReducer + private val reducer = BasicScreenReducer @Test fun `Ui event is executed`() { diff --git a/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/elm/TimerReducer.kt b/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/elm/TimerReducer.kt index 769f5f1d..1cdf42a9 100644 --- a/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/elm/TimerReducer.kt +++ b/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/elm/TimerReducer.kt @@ -1,9 +1,9 @@ package money.vivid.elmslie.samples.coroutines.timer.elm -import money.vivid.elmslie.core.store.dsl.DslReducer +import money.vivid.elmslie.core.store.StateReducer import java.util.UUID -internal object TimerReducer : DslReducer<Event, State, Effect, Command>() { +internal object TimerReducer : StateReducer<Event, State, Effect, Command>() { 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<Event, State, Effect, Command>() { + 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) - ) } } }