Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow split state definitions #29

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
9 changes: 8 additions & 1 deletion src/main/kotlin/com/tinder/StateMachine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,14 @@ class StateMachine<STATE : Any, EVENT : Any, SIDE_EFFECT : Any> private construc
private fun STATE.getDefinition() = graph.stateDefinitions
.filter { it.key.matches(this) }
.map { it.value }
.firstOrNull() ?: error("Missing definition for state ${this.javaClass.simpleName}!")
.also { if (it.isEmpty()) error("Missing definition for state ${this.javaClass.simpleName}!") }
.fold(Graph.State<STATE, EVENT, SIDE_EFFECT>()) { acc, state ->
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this change, this also works when we want to have a generic transition

private val stateMachine = StateMachine.create<String, Int, Nothing> {
                initialState(STATE_A)
                state(STATE_A) {
                    on(EVENT_1) {
                        transitionTo(STATE_B)
                    }
                }
                state(STATE_B) {
                    on(EVENT_2) {
                        transitionTo(STATE_A)
                    }
                }
                state<String> {
                    on(EVENT_3) {
                        transitionTo(STATE_C)
                    }
                }
            }

for example, this one, having state<String> or state(any()) we can move from any state to another state with the same event.

acc.apply {
onEnterListeners.addAll(state.onEnterListeners)
onExitListeners.addAll(state.onExitListeners)
transitions.putAll(state.transitions)
}
}

private fun STATE.notifyOnEnter(cause: EVENT) {
getDefinition().onEnterListeners.forEach { it(this, cause) }
Expand Down
78 changes: 77 additions & 1 deletion src/test/kotlin/com/tinder/StateMachineTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,83 @@ internal class StateMachineTest {
}
}

class WithSplitStateDefinition {
private val firstDefinitionOnEnterListener = mock<String.(Int) -> Unit>()
private val secondDefinitionOnEnterListener = mock<String.(Int) -> Unit>()
private val firstDefinitionOnExitListener = mock<String.(Int) -> Unit>()
private val secondDefinitionOnExitListener = mock<String.(Int) -> Unit>()

private val stateMachine = StateMachine.create<String, Int, Nothing> {
initialState(STATE_A)

state(STATE_A) {
on(EVENT_1) {
transitionTo(STATE_B)
}

onExit(firstDefinitionOnExitListener)
}

state(STATE_A) {
on(EVENT_2) {
transitionTo(STATE_B)
}

onExit(secondDefinitionOnExitListener)
}

state(STATE_B) {
onEnter(firstDefinitionOnEnterListener)
}

state(STATE_B) {
onEnter(secondDefinitionOnEnterListener)
}
}

@Test
fun transition_givenFirstDefinitionEvent_shouldReturnValidTransition() {
// When
val transition = stateMachine.transition(EVENT_1)

// Then
assertThat(transition).isEqualTo(
StateMachine.Transition.Valid<String, Int, String>(STATE_A, EVENT_1, STATE_B, null)
)
}

@Test
fun transition_givenSecondDefinitionEvent_shouldReturnValidTransition() {
// When
val transition = stateMachine.transition(EVENT_2)

// Then
assertThat(transition).isEqualTo(
StateMachine.Transition.Valid<String, Int, String>(STATE_A, EVENT_2, STATE_B, null)
)
}

@Test
fun transition_givenValidEvent_shouldTriggerOnExitListeners() {
// When
stateMachine.transition(EVENT_1)

// Then
then(firstDefinitionOnExitListener).should().invoke(STATE_A, EVENT_1)
then(secondDefinitionOnExitListener).should().invoke(STATE_A, EVENT_1)
}

@Test
fun transition_givenValidEvent_shouldTriggerOnEnterListeners() {
// When
stateMachine.transition(EVENT_1)

// Then
then(firstDefinitionOnEnterListener).should().invoke(STATE_B, EVENT_1)
then(secondDefinitionOnEnterListener).should().invoke(STATE_B, EVENT_1)
}
}

private companion object {
private const val STATE_A = "a"
private const val STATE_B = "b"
Expand All @@ -733,5 +810,4 @@ internal class StateMachineTest {
private const val SIDE_EFFECT_1 = "alpha"
}
}

}