diff --git a/features/verifysession/impl/build.gradle.kts b/features/verifysession/impl/build.gradle.kts index 09ebbd3ca5..965179ed28 100644 --- a/features/verifysession/impl/build.gradle.kts +++ b/features/verifysession/impl/build.gradle.kts @@ -39,7 +39,6 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.elementresources) implementation(projects.libraries.uiStrings) - implementation(projects.libraries.statemachine) api(libs.statemachine) api(projects.features.verifysession.api) diff --git a/libraries/statemachine/build.gradle.kts b/libraries/statemachine/build.gradle.kts deleted file mode 100644 index ba6aef4027..0000000000 --- a/libraries/statemachine/build.gradle.kts +++ /dev/null @@ -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) -} diff --git a/libraries/statemachine/src/main/kotlin/io/element/android/libraries/statemachine/StateMachine.kt b/libraries/statemachine/src/main/kotlin/io/element/android/libraries/statemachine/StateMachine.kt deleted file mode 100644 index dd8ea7114d..0000000000 --- a/libraries/statemachine/src/main/kotlin/io/element/android/libraries/statemachine/StateMachine.kt +++ /dev/null @@ -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 createStateMachine( - config: StateMachineBuilder.() -> Unit -): StateMachine { - val builder = StateMachineBuilder() - config(builder) - return builder.build() -} - -class StateMachine( - val initialState: State, - private val stateConfigs: Map, StateConfig<*>>, - private val routes: List>, -) { - - 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 - initialStateConfig.onEnter?.invoke(initialState) - } - - @Suppress("UNCHECKED_CAST") - fun process(event: E) { - val route = findMatchingRoute(event) ?: error("No route found for state $currentState on event $event") - - val lastStateConfig: StateConfig? = stateConfigs[currentState::class.java] as? StateConfig - 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 - currentStateConfig?.onEnter?.invoke(nextState) - } - - @Suppress("UNCHECKED_CAST") - private fun findMatchingRoute(event: E): StateMachineRoute? { - val routesForEvent = routes.filter { it.eventType.isInstance(event) } - - return (routesForEvent.firstOrNull { it.fromState?.isInstance(currentState) == true } - ?: routesForEvent.firstOrNull { it.fromState == null }) as? StateMachineRoute - } - - fun restart() { - _stateFlow.value = initialState - } -} - -class StateMachineBuilder( - val routes: MutableList> = mutableListOf(), -) { - - lateinit var initialState: State - var stateConfigs = mutableMapOf, StateConfig>() - - inline fun addState(block: StateRegistrationBuilder.() -> Unit = {}) { - val config = StateConfig(S::class.java) - val registrationBuilder = StateRegistrationBuilder(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 addInitialState(state: S, config: StateRegistrationBuilder.() -> Unit = {}) { - initialState = state - addState(block = config) - } - - inline fun on(noinline configuration: (E, State) -> S) { - val builder = RouteBuilder(E::class.java, null) - builder.toState = configuration - val newRoute = builder.build() - verifyRoutesAreUnique(S::class.java, routes, listOf(newRoute)) - routes.add(newRoute) - } - - inline fun on(newState: State) { - val builder = RouteBuilder(E::class.java, null) - builder.toState = { _, _ -> newState } - val newRoute = builder.build() - verifyRoutesAreUnique(null, routes, listOf(newRoute)) - routes.add(newRoute) - } - - fun build(): StateMachine { - 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>, - newRoutes: List>, - ) { - 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( - val fromState: StateConfig, - val routes: MutableList> = mutableListOf(), -) { - - fun onEnter(enter: (State) -> Unit) { - fromState.onEnter = enter - } - - fun onExit(exit: (State) -> Unit) { - fromState.onExit = exit - } - - inline fun on(noinline configuration: (E, State) -> BaseState) { - val builder = RouteBuilder(E::class.java, fromState.state) - builder.toState = configuration - val newRoute = builder.build() - StateMachineBuilder.verifyRoutesAreUnique(fromState.state, routes, listOf(newRoute)) - routes.add(newRoute) - } - - inline fun on(newState: BaseState) { - val builder = RouteBuilder(E::class.java, fromState.state) - builder.toState = { _, _ -> newState } - val newRoute = builder.build() - StateMachineBuilder.verifyRoutesAreUnique(fromState.state, routes, listOf(newRoute)) - routes.add(newRoute) - } -} - -class RouteBuilder( - val eventType: Class, - val fromState: Class?, -) { - lateinit var toState: (Event, FromState) -> ToState - - fun build() = StateMachineRoute(eventType, fromState, toState) -} - -data class StateMachineRoute( - val eventType: Class, - val fromState: Class?, - val toState: (Event, FromState) -> ToState, -) - -data class StateConfig( - val state: Class, - var onEnter: ((State) -> Unit)? = null, - var onExit: ((State) -> Unit)? = null, -) diff --git a/libraries/statemachine/src/test/kotlin/io/element/android/libraries/statemachine/StateMachineTests.kt b/libraries/statemachine/src/test/kotlin/io/element/android/libraries/statemachine/StateMachineTests.kt deleted file mode 100644 index 722ed1c004..0000000000 --- a/libraries/statemachine/src/test/kotlin/io/element/android/libraries/statemachine/StateMachineTests.kt +++ /dev/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? = null - private fun aStateMachine() = createStateMachine { - addInitialState(States.First) { - onExit { exitedFirstState = true } - on { first, _ -> - States.Second(first.string) - } - } - addState { - onEnter { enteredSecondState = true } - on(States.Third) - } - - addState() - - on { _, _ -> States.Fourth } - on(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(States.Canceled) - } - on(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(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 { - addState() - on(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 { - addInitialState(States.First) - addState() - } - }.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 { - addInitialState(States.First) { - on(States.Third) - on { _, _ -> 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 { - addInitialState(States.First) - on(States.Third) - on(States.Third) - } - }.onSuccess { - fail("It should have thrown an error") - }.onFailure { error -> - assertThat(error.message).startsWith("Duplicate registration in state") - } - Unit - } -} diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index fa601a998d..51c568960d 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -94,7 +94,6 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:dateformatter:impl")) implementation(project(":libraries:di")) implementation(project(":libraries:session-storage:impl")) - implementation(project(":libraries:statemachine")) implementation(project(":libraries:mediapickers:impl")) implementation(project(":libraries:mediaupload:impl")) implementation(project(":libraries:usersearch:impl"))