diff --git a/mvi/src/main/kotlin/com/adidas/mvi/State.kt b/mvi/src/main/kotlin/com/adidas/mvi/State.kt index 6fc583e..337cf36 100644 --- a/mvi/src/main/kotlin/com/adidas/mvi/State.kt +++ b/mvi/src/main/kotlin/com/adidas/mvi/State.kt @@ -2,4 +2,4 @@ package com.adidas.mvi import com.adidas.mvi.sideeffects.SideEffects -public data class State(val view: TState, val sideEffects: SideEffects) : LoggableState +public data class State(val view: TView, val sideEffects: SideEffects) : LoggableState diff --git a/mvi/src/main/kotlin/com/adidas/mvi/reducer/ReducerExtensions.kt b/mvi/src/main/kotlin/com/adidas/mvi/reducer/ReducerExtensions.kt index b2e48a1..5589c0e 100644 --- a/mvi/src/main/kotlin/com/adidas/mvi/reducer/ReducerExtensions.kt +++ b/mvi/src/main/kotlin/com/adidas/mvi/reducer/ReducerExtensions.kt @@ -26,3 +26,11 @@ public fun Reducer( defaultDispatcher = defaultDispatcher ) } + +@Suppress("UNCHECKED_CAST") +public inline fun Reducer<*, *>.requireView(): TView = + (state.value as State).view.apply { + if (this.javaClass != TView::class.java) { + throw ClassCastException("Required view of ${TView::class.java} type, but found ${this.javaClass}") + } + } diff --git a/mvi/src/main/kotlin/com/adidas/mvi/transform/SideEffectTransform.kt b/mvi/src/main/kotlin/com/adidas/mvi/transform/SideEffectTransform.kt index 94d34fe..ae44f63 100644 --- a/mvi/src/main/kotlin/com/adidas/mvi/transform/SideEffectTransform.kt +++ b/mvi/src/main/kotlin/com/adidas/mvi/transform/SideEffectTransform.kt @@ -3,12 +3,12 @@ package com.adidas.mvi.transform import com.adidas.mvi.State import com.adidas.mvi.sideeffects.SideEffects -public abstract class SideEffectTransform : - StateTransform> { +public abstract class SideEffectTransform : + StateTransform> { protected abstract fun mutate(sideEffects: SideEffects): SideEffects - final override fun reduce(currentState: State): State { + final override fun reduce(currentState: State): State { val sideEffects = currentState.sideEffects return currentState.copy(sideEffects = mutate(sideEffects)) } diff --git a/mvi/src/main/kotlin/com/adidas/mvi/transform/StateTransform.kt b/mvi/src/main/kotlin/com/adidas/mvi/transform/StateTransform.kt index 34db49b..007d050 100644 --- a/mvi/src/main/kotlin/com/adidas/mvi/transform/StateTransform.kt +++ b/mvi/src/main/kotlin/com/adidas/mvi/transform/StateTransform.kt @@ -4,7 +4,10 @@ import com.adidas.mvi.Loggable import kotlinx.coroutines.CoroutineDispatcher public interface StateTransform : Loggable { - public suspend fun reduce(currentState: TState, defaultDispatcher: CoroutineDispatcher): TState { + public suspend fun reduce( + currentState: TState, + defaultDispatcher: CoroutineDispatcher + ): TState { return this.reduce(currentState) } diff --git a/mvi/src/main/kotlin/com/adidas/mvi/transform/Transform.kt b/mvi/src/main/kotlin/com/adidas/mvi/transform/Transform.kt deleted file mode 100644 index e69de29..0000000 diff --git a/mvi/src/main/kotlin/com/adidas/mvi/transform/ViewTransform.kt b/mvi/src/main/kotlin/com/adidas/mvi/transform/ViewTransform.kt new file mode 100644 index 0000000..d7a7c39 --- /dev/null +++ b/mvi/src/main/kotlin/com/adidas/mvi/transform/ViewTransform.kt @@ -0,0 +1,12 @@ +package com.adidas.mvi.transform + +import com.adidas.mvi.State + +public abstract class ViewTransform : StateTransform> { + + protected abstract fun mutate(currentState: TView): TView + + final override fun reduce(currentState: State): State { + return currentState.copy(view = mutate(currentState.view)) + } +} diff --git a/mvi/src/test/kotlin/com/adidas/mvi/product/FakeProductStateTransform.kt b/mvi/src/test/kotlin/com/adidas/mvi/product/FakeProductStateTransform.kt deleted file mode 100644 index 2bf58f3..0000000 --- a/mvi/src/test/kotlin/com/adidas/mvi/product/FakeProductStateTransform.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.adidas.mvi.product - -import com.adidas.mvi.State -import com.adidas.mvi.transform.StateTransform - -class FakeProductStateTransform(private val state: State) : - StateTransform { - - override fun reduce(currentState: ProductState): ProductState { - return state.view - } -} diff --git a/mvi/src/test/kotlin/com/adidas/mvi/product/FakeProductViewTransform.kt b/mvi/src/test/kotlin/com/adidas/mvi/product/FakeProductViewTransform.kt new file mode 100644 index 0000000..5926991 --- /dev/null +++ b/mvi/src/test/kotlin/com/adidas/mvi/product/FakeProductViewTransform.kt @@ -0,0 +1,12 @@ +package com.adidas.mvi.product + +import com.adidas.mvi.State +import com.adidas.mvi.transform.ViewTransform + +class FakeProductViewTransform(private val state: State) : + ViewTransform() { + + override fun mutate(currentState: ProductState): ProductState { + return state.view + } +} diff --git a/mvi/src/test/kotlin/com/adidas/mvi/reducer/ReducerTests.kt b/mvi/src/test/kotlin/com/adidas/mvi/reducer/ReducerTests.kt index 5bab911..01a3ecd 100644 --- a/mvi/src/test/kotlin/com/adidas/mvi/reducer/ReducerTests.kt +++ b/mvi/src/test/kotlin/com/adidas/mvi/reducer/ReducerTests.kt @@ -2,12 +2,14 @@ package com.adidas.mvi.reducer import com.adidas.mvi.CoroutineListener import com.adidas.mvi.Reducer +import com.adidas.mvi.State import com.adidas.mvi.TerminatedIntentException import com.adidas.mvi.reducer.logger.SpyLogger import com.adidas.mvi.reducer.logger.shouldContainFailingIntent import com.adidas.mvi.reducer.logger.shouldContainFailingTransform import com.adidas.mvi.reducer.logger.shouldContainSuccessfulIntent import com.adidas.mvi.reducer.logger.shouldContainSuccessfulTransform +import com.adidas.mvi.sideeffects.SideEffects import com.adidas.mvi.transform.StateTransform import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.IsolationMode @@ -31,9 +33,13 @@ internal class ReducerTests : BehaviorSpec({ val coroutineListener = CoroutineListener() listeners(coroutineListener) - fun createReducer(executor: (intent: TestIntent) -> Flow> = createIntentExecutorContainer(transform = TestTransform.Transform1)) = Reducer( + fun createReducer( + executor: (intent: TestIntent) -> Flow>> = createIntentExecutorContainer( + transform = TestTransform.Transform1 + ) + ) = Reducer( coroutineScope = TestScope(coroutineListener.testCoroutineDispatcher), - initialState = TestState.InitialState, + initialState = State(TestState.InitialState, SideEffects()), defaultDispatcher = coroutineListener.testCoroutineDispatcher, logger = logger, intentExecutor = executor @@ -41,30 +47,31 @@ internal class ReducerTests : BehaviorSpec({ Given("A reducer with a initial state") { val executedIntents = mutableListOf() - val executor: (intent: TestIntent) -> Flow> = createIntentExecutorContainer(executedIntents) + val executor: (intent: TestIntent) -> Flow>> = + createIntentExecutorContainer(executedIntents) val reducer = createReducer(executor) When("I listen to its first value") { val firstValue = reducer.state.value Then("It should be the initial state") { - firstValue shouldBe TestState.InitialState + firstValue.view shouldBe TestState.InitialState } } When("I try to require a state different than the current state") { Then("It should cause a ClassCastException") { shouldThrow { - reducer.requireState() + reducer.requireView() } } } When("I try to require a state equal to the current state") { - val requiredState = reducer.requireState() + val requiredView = reducer.requireView() Then("It should return the cast state") { - requiredState shouldBe TestState.InitialState + requiredView shouldBe TestState.InitialState } } @@ -122,7 +129,7 @@ internal class ReducerTests : BehaviorSpec({ reducer.executeIntent(TestIntent.Transform1Producer) Then("The state should change to the state which Transform1 produces") { - reducer.state.value shouldBe TestState.StateFromTransform1 + reducer.state.value.view shouldBe TestState.StateFromTransform1 logger.history.shouldForOne { log -> log shouldContainSuccessfulTransform TestState.StateFromTransform1.toString() } @@ -138,7 +145,7 @@ internal class ReducerTests : BehaviorSpec({ reducer.executeIntent(TestIntent.FailedTransformProducer) Then("The current state should remain the same") { - reducer.state.value shouldBe TestState.InitialState + reducer.state.value.view shouldBe TestState.InitialState } Then("The failure should be logged") { @@ -163,7 +170,7 @@ internal class ReducerTests : BehaviorSpec({ testFlow.emit(0) Then("State must have been correctly changed") { - reducerWrapper.state.value.shouldBe(TestState.CainState) + reducerWrapper.state.value.view.shouldBe(TestState.CainState) } } @@ -173,30 +180,34 @@ internal class ReducerTests : BehaviorSpec({ testFlow.emit(0) Then("Only the last intent must transform the state") { - reducerWrapper.state.value.shouldBe(TestState.UniqueTransformState) + reducerWrapper.state.value.view.shouldBe(TestState.UniqueTransformState) } } } }) -private fun createIntentExecutorContainer(executedIntents: MutableList = mutableListOf()): (TestIntent) -> Flow> = +private fun createIntentExecutorContainer(executedIntents: MutableList = mutableListOf()): (TestIntent) -> Flow>> = { executedIntents.add(it) flowOf(TestTransform.Transform1) } -private fun createIntentExecutorContainer(exception: java.lang.Exception): (TestIntent) -> Flow> = +private fun createIntentExecutorContainer(exception: java.lang.Exception): (TestIntent) -> Flow>> = { if (it is TestIntent.SimpleIntent) throw exception emptyFlow() } -private fun createIntentExecutorContainer(intent: TestIntent = TestIntent.FailedTransformProducer, transform: TestTransform): (TestIntent) -> Flow> = +private fun createIntentExecutorContainer( + intent: TestIntent = TestIntent.FailedTransformProducer, + transform: TestTransform +): (TestIntent) -> Flow>> = { when (it) { intent -> { flowOf(transform) } + else -> emptyFlow() } } diff --git a/mvi/src/test/kotlin/com/adidas/mvi/reducer/TestCancellationReducerWrapper.kt b/mvi/src/test/kotlin/com/adidas/mvi/reducer/TestCancellationReducerWrapper.kt index 9269a15..d29fceb 100644 --- a/mvi/src/test/kotlin/com/adidas/mvi/reducer/TestCancellationReducerWrapper.kt +++ b/mvi/src/test/kotlin/com/adidas/mvi/reducer/TestCancellationReducerWrapper.kt @@ -2,7 +2,9 @@ package com.adidas.mvi.reducer import com.adidas.mvi.CoroutineListener import com.adidas.mvi.Reducer +import com.adidas.mvi.State import com.adidas.mvi.reducer.logger.SpyLogger +import com.adidas.mvi.sideeffects.SideEffects import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf @@ -17,7 +19,7 @@ internal class TestCancellationReducerWrapper( private val reducer = Reducer( coroutineScope = TestScope(coroutineListener.testCoroutineDispatcher), - initialState = TestState.InitialState, + initialState = State(TestState.InitialState, SideEffects()), defaultDispatcher = coroutineListener.testCoroutineDispatcher, logger = SpyLogger(), intentExecutor = this::executeIntent @@ -33,7 +35,12 @@ internal class TestCancellationReducerWrapper( ): Flow { return when (intent) { TestIntent.AbelIntent -> someTestFlow.map { TestTransform.AbelTransform } - TestIntent.CainIntent -> flowOf(TestTransform.CainTransform).onStart { reducer.cleanIntentJobsOfType(TestIntent.AbelIntent::class) } + TestIntent.CainIntent -> flowOf(TestTransform.CainTransform).onStart { + reducer.cleanIntentJobsOfType( + TestIntent.AbelIntent::class + ) + } + is TestIntent.UniqueTransformIntent -> someTestFlow.map { TestTransform.UniqueTransform(intent.id) } else -> emptyFlow() } diff --git a/mvi/src/test/kotlin/com/adidas/mvi/reducer/TestSideEffect.kt b/mvi/src/test/kotlin/com/adidas/mvi/reducer/TestSideEffect.kt new file mode 100644 index 0000000..8adda90 --- /dev/null +++ b/mvi/src/test/kotlin/com/adidas/mvi/reducer/TestSideEffect.kt @@ -0,0 +1,3 @@ +package com.adidas.mvi.reducer + +class TestSideEffect diff --git a/mvi/src/test/kotlin/com/adidas/mvi/reducer/TestTransform.kt b/mvi/src/test/kotlin/com/adidas/mvi/reducer/TestTransform.kt index 9a294d0..771594e 100644 --- a/mvi/src/test/kotlin/com/adidas/mvi/reducer/TestTransform.kt +++ b/mvi/src/test/kotlin/com/adidas/mvi/reducer/TestTransform.kt @@ -1,37 +1,37 @@ package com.adidas.mvi.reducer -import com.adidas.mvi.transform.StateTransform +import com.adidas.mvi.transform.ViewTransform internal const val IMPOSSIBLE_INTENT_ID = 1 internal const val UNIQUE_INTENT_ID = 2 -internal sealed class TestTransform : StateTransform { +internal sealed class TestTransform : ViewTransform() { object Transform1 : TestTransform() { - override fun reduce(currentState: TestState): TestState { + override fun mutate(currentState: TestState): TestState { return TestState.StateFromTransform1 } } object FailedTransform : TestTransform() { - override fun reduce(currentState: TestState): TestState { + override fun mutate(currentState: TestState): TestState { throw Exception() } } object AbelTransform : TestTransform() { - override fun reduce(currentState: TestState): TestState { + override fun mutate(currentState: TestState): TestState { return TestState.AbelState } } object CainTransform : TestTransform() { - override fun reduce(currentState: TestState): TestState { + override fun mutate(currentState: TestState): TestState { return TestState.CainState } } data class UniqueTransform(val id: Int) : TestTransform() { - override fun reduce(currentState: TestState): TestState { + override fun mutate(currentState: TestState): TestState { return when (id) { IMPOSSIBLE_INTENT_ID -> TestState.ImpossibleState UNIQUE_INTENT_ID -> TestState.UniqueTransformState diff --git a/mvi/src/test/kotlin/com/adidas/mvi/transform/StateTransformTest.kt b/mvi/src/test/kotlin/com/adidas/mvi/transform/StateTransformTest.kt index 52797ea..0056fde 100644 --- a/mvi/src/test/kotlin/com/adidas/mvi/transform/StateTransformTest.kt +++ b/mvi/src/test/kotlin/com/adidas/mvi/transform/StateTransformTest.kt @@ -1,7 +1,7 @@ package com.adidas.mvi.transform import com.adidas.mvi.State -import com.adidas.mvi.product.FakeProductStateTransform +import com.adidas.mvi.product.FakeProductViewTransform import com.adidas.mvi.product.ProductSideEffect import com.adidas.mvi.product.ProductState import com.adidas.mvi.sideeffects.SideEffects @@ -15,7 +15,7 @@ class StateTransformTest : ShouldSpec({ should("use mutate() function for reducing state") { val state = State(ProductState.Loading, SideEffects()) - FakeProductStateTransform(state).reduce(state.view) shouldBe state.view + FakeProductViewTransform(state).reduce(state).view shouldBe state.view } } })