Remove internal state machine library
This commit is contained in:
parent
39602d8491
commit
2a15681151
5 changed files with 0 additions and 429 deletions
|
|
@ -39,7 +39,6 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.elementresources)
|
implementation(projects.libraries.elementresources)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.statemachine)
|
|
||||||
api(libs.statemachine)
|
api(libs.statemachine)
|
||||||
api(projects.features.verifysession.api)
|
api(projects.features.verifysession.api)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id("java-library")
|
|
||||||
id("com.android.lint")
|
|
||||||
alias(libs.plugins.kotlin.jvm)
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(libs.coroutines.core)
|
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
}
|
|
||||||
|
|
@ -1,192 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.statemachine
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
|
|
||||||
fun <Event : Any, State : Any> createStateMachine(
|
|
||||||
config: StateMachineBuilder<Event, State>.() -> Unit
|
|
||||||
): StateMachine<Event, State> {
|
|
||||||
val builder = StateMachineBuilder<Event, State>()
|
|
||||||
config(builder)
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
class StateMachine<Event : Any, State : Any>(
|
|
||||||
val initialState: State,
|
|
||||||
private val stateConfigs: Map<Class<*>, StateConfig<*>>,
|
|
||||||
private val routes: List<StateMachineRoute<*, *, *>>,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val _stateFlow = MutableStateFlow(initialState)
|
|
||||||
val stateFlow = _stateFlow.asStateFlow()
|
|
||||||
val currentState: State get() = stateFlow.value
|
|
||||||
|
|
||||||
var transitionHandler: ((State, Event, State) -> Unit)? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val initialStateConfig = stateConfigs[initialState::class.java] as StateConfig<State>
|
|
||||||
initialStateConfig.onEnter?.invoke(initialState)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <E : Event> process(event: E) {
|
|
||||||
val route = findMatchingRoute(event) ?: error("No route found for state $currentState on event $event")
|
|
||||||
|
|
||||||
val lastStateConfig: StateConfig<State>? = stateConfigs[currentState::class.java] as? StateConfig<State>
|
|
||||||
lastStateConfig?.onExit?.invoke(currentState)
|
|
||||||
|
|
||||||
val nextState = route.toState(event, currentState)
|
|
||||||
transitionHandler?.invoke(currentState, event, nextState)
|
|
||||||
_stateFlow.value = nextState
|
|
||||||
|
|
||||||
val currentStateConfig = stateConfigs[nextState::class.java] as? StateConfig<State>
|
|
||||||
currentStateConfig?.onEnter?.invoke(nextState)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
private fun <E : Event> findMatchingRoute(event: E): StateMachineRoute<E, State, State>? {
|
|
||||||
val routesForEvent = routes.filter { it.eventType.isInstance(event) }
|
|
||||||
|
|
||||||
return (routesForEvent.firstOrNull { it.fromState?.isInstance(currentState) == true }
|
|
||||||
?: routesForEvent.firstOrNull { it.fromState == null }) as? StateMachineRoute<E, State, State>
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restart() {
|
|
||||||
_stateFlow.value = initialState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StateMachineBuilder<Event : Any, State : Any>(
|
|
||||||
val routes: MutableList<StateMachineRoute<out Event, out State, out State>> = mutableListOf(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
lateinit var initialState: State
|
|
||||||
var stateConfigs = mutableMapOf<Class<out State>, StateConfig<out State>>()
|
|
||||||
|
|
||||||
inline fun <reified S : State> addState(block: StateRegistrationBuilder<Event, State, S>.() -> Unit = {}) {
|
|
||||||
val config = StateConfig(S::class.java)
|
|
||||||
val registrationBuilder = StateRegistrationBuilder<Event, State, S>(config)
|
|
||||||
block(registrationBuilder)
|
|
||||||
|
|
||||||
verifyRoutesAreUnique(S::class.java, routes, registrationBuilder.routes)
|
|
||||||
|
|
||||||
if (stateConfigs.contains(S::class.java)) {
|
|
||||||
error("Duplicate registration for state ${S::class.java.name}")
|
|
||||||
}
|
|
||||||
stateConfigs[S::class.java] = config
|
|
||||||
routes.addAll(registrationBuilder.routes)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified S : State> addInitialState(state: S, config: StateRegistrationBuilder<Event, State, S>.() -> Unit = {}) {
|
|
||||||
initialState = state
|
|
||||||
addState(block = config)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified E : Event, reified S : State> on(noinline configuration: (E, State) -> S) {
|
|
||||||
val builder = RouteBuilder<E, State, S>(E::class.java, null)
|
|
||||||
builder.toState = configuration
|
|
||||||
val newRoute = builder.build()
|
|
||||||
verifyRoutesAreUnique(S::class.java, routes, listOf(newRoute))
|
|
||||||
routes.add(newRoute)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified E : Event> on(newState: State) {
|
|
||||||
val builder = RouteBuilder<E, State, State>(E::class.java, null)
|
|
||||||
builder.toState = { _, _ -> newState }
|
|
||||||
val newRoute = builder.build()
|
|
||||||
verifyRoutesAreUnique(null, routes, listOf(newRoute))
|
|
||||||
routes.add(newRoute)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun build(): StateMachine<Event, State> {
|
|
||||||
if (::initialState.isInitialized) {
|
|
||||||
return StateMachine(initialState, stateConfigs.toMap(), routes)
|
|
||||||
} else {
|
|
||||||
error("The state machine has no initial state")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun verifyRoutesAreUnique(
|
|
||||||
state: Class<*>?,
|
|
||||||
oldRoutes: List<StateMachineRoute<*, *, *>>,
|
|
||||||
newRoutes: List<StateMachineRoute<*, *, *>>,
|
|
||||||
) {
|
|
||||||
val oldEvents = oldRoutes.filter { it.fromState == state }.map { it.eventType }
|
|
||||||
val newEvents = newRoutes.filter { it.fromState == state }.map { it.eventType }
|
|
||||||
val intersection = oldEvents.intersect(newEvents)
|
|
||||||
if (intersection.isNotEmpty()) {
|
|
||||||
val duplicates = intersection.joinToString(", ") { it.name }
|
|
||||||
error("Duplicate registration in state ${state?.name} for events: $duplicates")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StateRegistrationBuilder<Event : Any, BaseState : Any, State : BaseState>(
|
|
||||||
val fromState: StateConfig<State>,
|
|
||||||
val routes: MutableList<StateMachineRoute<out Event, out State, out BaseState>> = mutableListOf(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun onEnter(enter: (State) -> Unit) {
|
|
||||||
fromState.onEnter = enter
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onExit(exit: (State) -> Unit) {
|
|
||||||
fromState.onExit = exit
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified E : Event> on(noinline configuration: (E, State) -> BaseState) {
|
|
||||||
val builder = RouteBuilder<E, State, BaseState>(E::class.java, fromState.state)
|
|
||||||
builder.toState = configuration
|
|
||||||
val newRoute = builder.build()
|
|
||||||
StateMachineBuilder.verifyRoutesAreUnique(fromState.state, routes, listOf(newRoute))
|
|
||||||
routes.add(newRoute)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified E : Event> on(newState: BaseState) {
|
|
||||||
val builder = RouteBuilder<E, State, BaseState>(E::class.java, fromState.state)
|
|
||||||
builder.toState = { _, _ -> newState }
|
|
||||||
val newRoute = builder.build()
|
|
||||||
StateMachineBuilder.verifyRoutesAreUnique(fromState.state, routes, listOf(newRoute))
|
|
||||||
routes.add(newRoute)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RouteBuilder<Event : Any, FromState : Any, ToState : Any>(
|
|
||||||
val eventType: Class<out Event>,
|
|
||||||
val fromState: Class<out FromState>?,
|
|
||||||
) {
|
|
||||||
lateinit var toState: (Event, FromState) -> ToState
|
|
||||||
|
|
||||||
fun build() = StateMachineRoute(eventType, fromState, toState)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class StateMachineRoute<Event : Any, FromState : Any, ToState : Any>(
|
|
||||||
val eventType: Class<out Event>,
|
|
||||||
val fromState: Class<out FromState>?,
|
|
||||||
val toState: (Event, FromState) -> ToState,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class StateConfig<State : Any>(
|
|
||||||
val state: Class<State>,
|
|
||||||
var onEnter: ((State) -> Unit)? = null,
|
|
||||||
var onExit: ((State) -> Unit)? = null,
|
|
||||||
)
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.statemachine
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import org.junit.Assert.fail
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class StateMachineTests {
|
|
||||||
|
|
||||||
sealed interface Events {
|
|
||||||
data class GoToSecond(val string: String) : Events
|
|
||||||
|
|
||||||
object GoToThird : Events
|
|
||||||
|
|
||||||
object GoToFourth : Events
|
|
||||||
|
|
||||||
object Cancel : Events
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface States {
|
|
||||||
object First : States
|
|
||||||
|
|
||||||
data class Second(val string: String) : States
|
|
||||||
|
|
||||||
object Third : States
|
|
||||||
|
|
||||||
object Fourth : States
|
|
||||||
object Canceled : States
|
|
||||||
}
|
|
||||||
|
|
||||||
private var enteredSecondState = false
|
|
||||||
private var exitedFirstState = false
|
|
||||||
private var transitionHandlerParams: Triple<States, Events, States>? = null
|
|
||||||
private fun aStateMachine() = createStateMachine<Events, States> {
|
|
||||||
addInitialState(States.First) {
|
|
||||||
onExit { exitedFirstState = true }
|
|
||||||
on<Events.GoToSecond> { first, _ ->
|
|
||||||
States.Second(first.string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addState<States.Second> {
|
|
||||||
onEnter { enteredSecondState = true }
|
|
||||||
on<Events.GoToThird>(States.Third)
|
|
||||||
}
|
|
||||||
|
|
||||||
addState<States.Fourth>()
|
|
||||||
|
|
||||||
on<Events.GoToFourth, States.Fourth> { _, _ -> States.Fourth }
|
|
||||||
on<Events.Cancel>(States.Canceled)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `process - moves to next state given an event if the route exists`() = aStateMachine().run {
|
|
||||||
process(Events.GoToSecond("Hello"))
|
|
||||||
assertThat(currentState).isEqualTo(States.Second("Hello"))
|
|
||||||
process(Events.GoToThird)
|
|
||||||
assertThat(currentState).isEqualTo(States.Third)
|
|
||||||
process(Events.GoToFourth)
|
|
||||||
assertThat(currentState).isEqualTo(States.Fourth)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `process - throws exception if there is no route for an event in a state`() = aStateMachine().run {
|
|
||||||
runCatching {
|
|
||||||
process(Events.GoToThird)
|
|
||||||
}.onSuccess {
|
|
||||||
fail("It should have thrown an error")
|
|
||||||
}.onFailure {
|
|
||||||
assertThat(it.message).startsWith("No route found for state")
|
|
||||||
}
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `process - calls onEnter and onExit callbacks when moving through states`() = aStateMachine().run {
|
|
||||||
process(Events.GoToSecond("Hello"))
|
|
||||||
assertThat(currentState).isEqualTo(States.Second("Hello"))
|
|
||||||
|
|
||||||
assertThat(exitedFirstState).isTrue()
|
|
||||||
assertThat(enteredSecondState).isTrue()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `process - if an Event route is registered inside a state and outside it, the internal registration takes precedence`() {
|
|
||||||
val customStateMachine = createStateMachine {
|
|
||||||
addInitialState(States.First) {
|
|
||||||
on<Events.Cancel>(States.Canceled)
|
|
||||||
}
|
|
||||||
on<Events.Cancel>(States.Fourth)
|
|
||||||
}
|
|
||||||
customStateMachine.process(Events.Cancel)
|
|
||||||
assertThat(customStateMachine.currentState).isEqualTo(States.Canceled)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `transitionHandler - is called when moving from a state to another`() = aStateMachine().run {
|
|
||||||
transitionHandler = { from, event, to ->
|
|
||||||
transitionHandlerParams = Triple(from, event, to)
|
|
||||||
}
|
|
||||||
|
|
||||||
process(Events.GoToSecond("Hello"))
|
|
||||||
|
|
||||||
assertThat(transitionHandlerParams).isEqualTo(
|
|
||||||
Triple(
|
|
||||||
States.First,
|
|
||||||
Events.GoToSecond("Hello"),
|
|
||||||
States.Second("Hello"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `restart - sets the state machine to its initial state`() {
|
|
||||||
val customStateMachine = createStateMachine {
|
|
||||||
addInitialState(States.First)
|
|
||||||
on<Events.GoToFourth>(States.Fourth)
|
|
||||||
}
|
|
||||||
customStateMachine.process(Events.GoToFourth)
|
|
||||||
assertThat(customStateMachine.currentState).isEqualTo(States.Fourth)
|
|
||||||
|
|
||||||
customStateMachine.restart()
|
|
||||||
assertThat(customStateMachine.currentState).isEqualTo(customStateMachine.initialState)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `init - the state machine must have registered a initial state`() {
|
|
||||||
runCatching {
|
|
||||||
createStateMachine<Events, States> {
|
|
||||||
addState<States.Second>()
|
|
||||||
on<Events.Cancel>(States.Canceled)
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
fail("It should have thrown an error")
|
|
||||||
}.onFailure { error ->
|
|
||||||
assertThat(error.message).isEqualTo("The state machine has no initial state")
|
|
||||||
}
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `init - the state machine having duplicate registrations for a state throws an error`() {
|
|
||||||
runCatching {
|
|
||||||
createStateMachine<Events, States> {
|
|
||||||
addInitialState(States.First)
|
|
||||||
addState<States.First>()
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
fail("It should have thrown an error")
|
|
||||||
}.onFailure { error ->
|
|
||||||
assertThat(error.message).startsWith("Duplicate registration for state ")
|
|
||||||
}
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `init - the state machine having duplicate registrations for an event inside a state throws an error`() {
|
|
||||||
runCatching {
|
|
||||||
createStateMachine<Events, States> {
|
|
||||||
addInitialState(States.First) {
|
|
||||||
on<Events.GoToThird>(States.Third)
|
|
||||||
on<Events.GoToThird> { _, _ -> States.Third }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
fail("It should have thrown an error")
|
|
||||||
}.onFailure { error ->
|
|
||||||
assertThat(error.message).startsWith("Duplicate registration in state")
|
|
||||||
}
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `init - the state machine having duplicate registrations for an event at the root level throws an error`() {
|
|
||||||
runCatching {
|
|
||||||
createStateMachine<Events, States> {
|
|
||||||
addInitialState(States.First)
|
|
||||||
on<Events.GoToThird>(States.Third)
|
|
||||||
on<Events.GoToThird>(States.Third)
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
fail("It should have thrown an error")
|
|
||||||
}.onFailure { error ->
|
|
||||||
assertThat(error.message).startsWith("Duplicate registration in state")
|
|
||||||
}
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -94,7 +94,6 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
||||||
implementation(project(":libraries:dateformatter:impl"))
|
implementation(project(":libraries:dateformatter:impl"))
|
||||||
implementation(project(":libraries:di"))
|
implementation(project(":libraries:di"))
|
||||||
implementation(project(":libraries:session-storage:impl"))
|
implementation(project(":libraries:session-storage:impl"))
|
||||||
implementation(project(":libraries:statemachine"))
|
|
||||||
implementation(project(":libraries:mediapickers:impl"))
|
implementation(project(":libraries:mediapickers:impl"))
|
||||||
implementation(project(":libraries:mediaupload:impl"))
|
implementation(project(":libraries:mediaupload:impl"))
|
||||||
implementation(project(":libraries:usersearch:impl"))
|
implementation(project(":libraries:usersearch:impl"))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue