Skip to content

Commit

Permalink
Merge pull request #13 from adidas/enhancement/add-ViewTransform
Browse files Browse the repository at this point in the history
Add ViewTransform.kt + Rename TState to TView
  • Loading branch information
sherviiin authored Aug 8, 2022
2 parents ac17cc2 + 2f4a7d7 commit bfb9c0e
Show file tree
Hide file tree
Showing 13 changed files with 86 additions and 42 deletions.
2 changes: 1 addition & 1 deletion mvi/src/main/kotlin/com/adidas/mvi/State.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package com.adidas.mvi

import com.adidas.mvi.sideeffects.SideEffects

public data class State<out TState, TSideEffect>(val view: TState, val sideEffects: SideEffects<TSideEffect>) : LoggableState
public data class State<out TView, TSideEffect>(val view: TView, val sideEffects: SideEffects<TSideEffect>) : LoggableState
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ public fun <TIntent : Intent, TInnerState : LoggableState, TAction> Reducer(
defaultDispatcher = defaultDispatcher
)
}

@Suppress("UNCHECKED_CAST")
public inline fun <reified TView : Any> Reducer<*, *>.requireView(): TView =
(state.value as State<TView, *>).view.apply {
if (this.javaClass != TView::class.java) {
throw ClassCastException("Required view of ${TView::class.java} type, but found ${this.javaClass}")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package com.adidas.mvi.transform
import com.adidas.mvi.State
import com.adidas.mvi.sideeffects.SideEffects

public abstract class SideEffectTransform<TState, TSideEffect> :
StateTransform<State<TState, TSideEffect>> {
public abstract class SideEffectTransform<TView, TSideEffect> :
StateTransform<State<TView, TSideEffect>> {

protected abstract fun mutate(sideEffects: SideEffects<TSideEffect>): SideEffects<TSideEffect>

final override fun reduce(currentState: State<TState, TSideEffect>): State<TState, TSideEffect> {
final override fun reduce(currentState: State<TView, TSideEffect>): State<TView, TSideEffect> {
val sideEffects = currentState.sideEffects
return currentState.copy(sideEffects = mutate(sideEffects))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import com.adidas.mvi.Loggable
import kotlinx.coroutines.CoroutineDispatcher

public interface StateTransform<TState> : Loggable {
public suspend fun reduce(currentState: TState, defaultDispatcher: CoroutineDispatcher): TState {
public suspend fun reduce(
currentState: TState,
defaultDispatcher: CoroutineDispatcher
): TState {
return this.reduce(currentState)
}

Expand Down
Empty file.
12 changes: 12 additions & 0 deletions mvi/src/main/kotlin/com/adidas/mvi/transform/ViewTransform.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.adidas.mvi.transform

import com.adidas.mvi.State

public abstract class ViewTransform<TView, TSideEffect> : StateTransform<State<TView, TSideEffect>> {

protected abstract fun mutate(currentState: TView): TView

final override fun reduce(currentState: State<TView, TSideEffect>): State<TView, TSideEffect> {
return currentState.copy(view = mutate(currentState.view))
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<ProductState, ProductSideEffect>) :
ViewTransform<ProductState, ProductSideEffect>() {

override fun mutate(currentState: ProductState): ProductState {
return state.view
}
}
39 changes: 25 additions & 14 deletions mvi/src/test/kotlin/com/adidas/mvi/reducer/ReducerTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,40 +33,45 @@ internal class ReducerTests : BehaviorSpec({
val coroutineListener = CoroutineListener()
listeners(coroutineListener)

fun createReducer(executor: (intent: TestIntent) -> Flow<StateTransform<TestState>> = createIntentExecutorContainer(transform = TestTransform.Transform1)) = Reducer(
fun createReducer(
executor: (intent: TestIntent) -> Flow<StateTransform<State<TestState, TestSideEffect>>> = createIntentExecutorContainer(
transform = TestTransform.Transform1
)
) = Reducer(
coroutineScope = TestScope(coroutineListener.testCoroutineDispatcher),
initialState = TestState.InitialState,
initialState = State(TestState.InitialState, SideEffects()),
defaultDispatcher = coroutineListener.testCoroutineDispatcher,
logger = logger,
intentExecutor = executor
)

Given("A reducer with a initial state") {
val executedIntents = mutableListOf<TestIntent>()
val executor: (intent: TestIntent) -> Flow<StateTransform<TestState>> = createIntentExecutorContainer(executedIntents)
val executor: (intent: TestIntent) -> Flow<StateTransform<State<TestState, TestSideEffect>>> =
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<ClassCastException> {
reducer.requireState<TestState.StateFromTransform1>()
reducer.requireView<TestState.StateFromTransform1>()
}
}
}

When("I try to require a state equal to the current state") {
val requiredState = reducer.requireState<TestState.InitialState>()
val requiredView = reducer.requireView<TestState.InitialState>()

Then("It should return the cast state") {
requiredState shouldBe TestState.InitialState
requiredView shouldBe TestState.InitialState
}
}

Expand Down Expand Up @@ -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()
}
Expand All @@ -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") {
Expand All @@ -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)
}
}

Expand All @@ -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<TestIntent> = mutableListOf()): (TestIntent) -> Flow<StateTransform<TestState>> =
private fun createIntentExecutorContainer(executedIntents: MutableList<TestIntent> = mutableListOf()): (TestIntent) -> Flow<StateTransform<State<TestState, TestSideEffect>>> =
{
executedIntents.add(it)
flowOf(TestTransform.Transform1)
}

private fun createIntentExecutorContainer(exception: java.lang.Exception): (TestIntent) -> Flow<StateTransform<TestState>> =
private fun createIntentExecutorContainer(exception: java.lang.Exception): (TestIntent) -> Flow<StateTransform<State<TestState, TestSideEffect>>> =
{
if (it is TestIntent.SimpleIntent) throw exception
emptyFlow()
}

private fun createIntentExecutorContainer(intent: TestIntent = TestIntent.FailedTransformProducer, transform: TestTransform): (TestIntent) -> Flow<StateTransform<TestState>> =
private fun createIntentExecutorContainer(
intent: TestIntent = TestIntent.FailedTransformProducer,
transform: TestTransform
): (TestIntent) -> Flow<StateTransform<State<TestState, TestSideEffect>>> =
{
when (it) {
intent -> {
flowOf(transform)
}

else -> emptyFlow()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -33,7 +35,12 @@ internal class TestCancellationReducerWrapper(
): Flow<TestTransform> {
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()
}
Expand Down
3 changes: 3 additions & 0 deletions mvi/src/test/kotlin/com/adidas/mvi/reducer/TestSideEffect.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.adidas.mvi.reducer

class TestSideEffect
14 changes: 7 additions & 7 deletions mvi/src/test/kotlin/com/adidas/mvi/reducer/TestTransform.kt
Original file line number Diff line number Diff line change
@@ -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<TestState> {
internal sealed class TestTransform : ViewTransform<TestState, TestSideEffect>() {
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,7 +15,7 @@ class StateTransformTest : ShouldSpec({

should("use mutate() function for reducing state") {
val state = State(ProductState.Loading, SideEffects<ProductSideEffect>())
FakeProductStateTransform(state).reduce(state.view) shouldBe state.view
FakeProductViewTransform(state).reduce(state).view shouldBe state.view
}
}
})

0 comments on commit bfb9c0e

Please sign in to comment.