rework of FSM + 0.8.0

This commit is contained in:
InsanusMokrassar 2021-11-04 15:31:10 +06:00
parent 00acb9fddd
commit bbe5320312
14 changed files with 156 additions and 250 deletions

View File

@ -1,6 +1,6 @@
# Changelog
## 0.7.5
## 0.8.0
* `Versions`:
* `Klock`: `2.4.6` -> `2.4.7`
@ -9,6 +9,9 @@
* Type `Either` got its own serializer
* `FSM`:
* `Common`:
* Full rework of FSM:
* Now it is more flexible for checking of handler opportunity to handle state
* Now machine and states managers are type-oriented
* Add opportunity for comfortable adding default state handler
## 0.7.4

View File

@ -1,6 +0,0 @@
package dev.inmo.micro_utils.fsm.common
import dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager
@Deprecated("Replaced", ReplaceWith("InMemoryStatesManager", "dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager"))
typealias InMemoryStatesManager = InMemoryStatesManager

View File

@ -3,10 +3,10 @@ package dev.inmo.micro_utils.fsm.common
/**
* Default realization of states handler
*/
fun interface StatesHandler<I : State> {
fun interface StatesHandler<I : State, O: State> {
/**
* Main handling of [state]. In case when this [state] leads to another [State] and [handleState] returns not null
* [State] it is assumed that chain is not completed.
*/
suspend fun StatesMachine.handleState(state: I): State?
suspend fun StatesMachine<in O>.handleState(state: I): O?
}

View File

@ -2,37 +2,67 @@ package dev.inmo.micro_utils.fsm.common
import kotlin.reflect.KClass
/**
* Define checkable holder which can be used to precheck that this handler may handle incoming [State]
*/
interface CheckableHandlerHolder<I : State, O : State> : StatesHandler<I, O> {
suspend fun checkHandleable(state: O): Boolean
}
/**
* Default realization of [StatesHandler]. It will incapsulate checking of [State] type in [checkHandleable] and class
* casting in [handleState]
*/
class StatesHandlerHolder<I : State>(
private val inputKlass: KClass<I>,
private val strict: Boolean = false,
private val delegateTo: StatesHandler<I>
) : StatesHandler<State> {
class CustomizableHandlerHolder<I : O, O : State>(
private val delegateTo: StatesHandler<I, O>,
private val filter: suspend (state: O) -> Boolean
) : CheckableHandlerHolder<I, O> {
/**
* Checks that [state] can be handled by [delegateTo]. Under the hood it will check exact equality of [state]
* [KClass] and use [KClass.isInstance] of [inputKlass] if [strict] == false
*/
fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state))
override suspend fun checkHandleable(state: O) = filter(state)
/**
* Calls [delegateTo] method [StatesHandler.handleState] with [state] casted to [I]. Use [checkHandleable]
* to be sure that this [StatesHandlerHolder] will be able to handle [state]
*/
override suspend fun StatesMachine.handleState(state: State): State? {
return delegateTo.run { handleState(state as I) }
override suspend fun StatesMachine<in O>.handleState(state: I): O? {
return delegateTo.run { handleState(state) }
}
}
@Deprecated("Renamed", ReplaceWith("StatesHandlerHolder"))
typealias StateHandlerHolder<T> = StatesHandlerHolder<T>
fun <I : O, O : State> StateHandlerHolder(
inputKlass: KClass<I>,
strict: Boolean = false,
delegateTo: StatesHandler<I, O>
) = CustomizableHandlerHolder(
StatesHandler<O, O> {
delegateTo.run { handleState(it as I) }
},
if (strict) {
{ it::class == inputKlass }
} else {
{ inputKlass.isInstance(it) }
}
)
inline fun <reified T : State> StatesHandler<T>.holder(
inline fun <reified I : O, O : State> StateHandlerHolder(
strict: Boolean = false,
delegateTo: StatesHandler<I, O>
) = StateHandlerHolder(I::class, strict, delegateTo)
inline fun <reified I : O, O: State> StatesHandler<I, O>.holder(
strict: Boolean = true
) = StatesHandlerHolder(
T::class,
) = StateHandlerHolder<I, O>(
I::class,
strict,
this
)
inline fun <I : O, O: State> StatesHandler<I, O>.holder(
noinline filter: suspend (state: State) -> Boolean
) = CustomizableHandlerHolder<O, O>(
{ this@holder.run { handleState(it as I) } },
filter
)

View File

@ -3,21 +3,21 @@ package dev.inmo.micro_utils.fsm.common
import dev.inmo.micro_utils.coroutines.*
import kotlinx.coroutines.*
private suspend fun <I : State> StatesMachine.launchStateHandling(
state: State,
handlers: List<StatesHandlerHolder<out I>>
): State? {
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
handleState(state)
}
}
/**
* Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
* [DefaultStatesMachine] or build it with [dev.inmo.micro_utils.fsm.common.dsl.buildFSM]. Implementers MUST NOT start
* handling until [start] method will be called
*/
interface StatesMachine : StatesHandler<State> {
interface StatesMachine<T : State> : StatesHandler<T, T> {
suspend fun launchStateHandling(
state: T,
handlers: List<CustomizableHandlerHolder<in T, T>>
): T? {
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
handleState(state)
}
}
/**
* Starts handling of [State]s
*/
@ -26,15 +26,15 @@ interface StatesMachine : StatesHandler<State> {
/**
* Start chain of [State]s witn [state]
*/
suspend fun startChain(state: State)
suspend fun startChain(state: T)
companion object {
/**
* Creates [DefaultStatesMachine]
*/
operator fun invoke(
statesManager: StatesManager,
handlers: List<StatesHandlerHolder<*>>
operator fun <T: State> invoke(
statesManager: StatesManager<T>,
handlers: List<CustomizableHandlerHolder<in T, T>>
) = DefaultStatesMachine(statesManager, handlers)
}
}
@ -43,14 +43,14 @@ interface StatesMachine : StatesHandler<State> {
* Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
* resolving, and uses [launchStateHandling] for [State] handling
*/
class DefaultStatesMachine (
private val statesManager: StatesManager,
private val handlers: List<StatesHandlerHolder<*>>
) : StatesMachine {
class DefaultStatesMachine <T: State>(
private val statesManager: StatesManager<T>,
private val handlers: List<CustomizableHandlerHolder<in T, T>>
) : StatesMachine<T> {
/**
* Will call [launchStateHandling] for state handling
*/
override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers)
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
/**
* Launch handling of states. On [statesManager] [StatesManager.onStartChain],
@ -59,7 +59,7 @@ class DefaultStatesMachine (
* [StatesManager.endChain].
*/
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
val statePerformer: suspend (State) -> Unit = { state: State ->
val statePerformer: suspend (T) -> Unit = { state: T ->
val newState = launchStateHandling(state, handlers)
if (newState != null) {
statesManager.update(state, newState)
@ -82,7 +82,7 @@ class DefaultStatesMachine (
/**
* Just calls [StatesManager.startChain] of [statesManager]
*/
override suspend fun startChain(state: State) {
override suspend fun startChain(state: T) {
statesManager.startChain(state)
}
}

View File

@ -2,29 +2,29 @@ package dev.inmo.micro_utils.fsm.common
import kotlinx.coroutines.flow.*
interface StatesManager {
val onChainStateUpdated: Flow<Pair<State, State>>
val onStartChain: Flow<State>
val onEndChain: Flow<State>
interface StatesManager<T : State> {
val onChainStateUpdated: Flow<Pair<T, T>>
val onStartChain: Flow<T>
val onEndChain: Flow<T>
/**
* Must set current set using [State.context]
*/
suspend fun update(old: State, new: State)
suspend fun update(old: T, new: T)
/**
* Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already
* busy by the other [State]
*/
suspend fun startChain(state: State)
suspend fun startChain(state: T)
/**
* Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just
* ignored
*/
suspend fun endChain(state: State)
suspend fun endChain(state: T)
suspend fun getActiveStates(): List<State>
suspend fun getActiveStates(): List<T>
}

View File

@ -4,36 +4,47 @@ import dev.inmo.micro_utils.fsm.common.*
import dev.inmo.micro_utils.fsm.common.managers.*
import kotlin.reflect.KClass
class FSMBuilder(
var statesManager: StatesManager = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()),
var defaultStateHandler: StatesHandler<State>? = StatesHandler { null }
class FSMBuilder<T : State>(
var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()),
var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null }
) {
private var states = mutableListOf<StatesHandlerHolder<*>>()
private var states = mutableListOf<CustomizableHandlerHolder<T, T>>()
fun <I : State> add(kClass: KClass<I>, handler: StatesHandler<I>) {
states.add(StatesHandlerHolder(kClass, false, handler))
fun <I : T> add(kClass: KClass<I>, handler: StatesHandler<I, T>) {
states.add(StateHandlerHolder(kClass, false, handler))
}
fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) {
states.add(StatesHandlerHolder(kClass, true, handler))
fun <I : T> add(filter: suspend (state: State) -> Boolean, handler: StatesHandler<I, T>) {
states.add(handler.holder(filter))
}
fun <I : T> addStrict(kClass: KClass<I>, handler: StatesHandler<I, T>) {
states.add(StateHandlerHolder(kClass, true, handler))
}
inline fun <reified I : T> onStateOrSubstate(handler: StatesHandler<I, T>) {
add(I::class, handler)
}
inline fun <reified I : T> strictlyOn(handler: StatesHandler<I, T>) {
addStrict(I::class, handler)
}
inline fun <reified I : T> doWhen(
noinline filter: suspend (state: State) -> Boolean,
handler: StatesHandler<I, T>
) {
add(filter, handler)
}
fun build() = StatesMachine(
statesManager,
states.toList().let { list ->
defaultStateHandler ?.let { list + it.holder(false) } ?: list
defaultStateHandler ?.let { list + it.holder { true } } ?: list
}
)
}
inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) {
add(I::class, handler)
}
inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) {
addStrict(I::class, handler)
}
fun buildFSM(
block: FSMBuilder.() -> Unit
): StatesMachine = FSMBuilder().apply(block).build()
fun <T : State> buildFSM(
block: FSMBuilder<T>.() -> Unit
): StatesMachine<T> = FSMBuilder<T>().apply(block).build()

View File

@ -9,25 +9,25 @@ import kotlinx.coroutines.sync.withLock
/**
* Implement this repo if you want to use some custom repo for [DefaultStatesManager]
*/
interface DefaultStatesManagerRepo {
interface DefaultStatesManagerRepo<T : State> {
/**
* Must save [state] as current state of chain with [State.context] of [state]
*/
suspend fun set(state: State)
suspend fun set(state: T)
/**
* Remove exactly [state]. In case if internally [State.context] is busy with different [State], that [State] should
* NOT be removed
*/
suspend fun removeState(state: State)
suspend fun removeState(state: T)
/**
* @return Current list of available and saved states
*/
suspend fun getStates(): List<State>
suspend fun getStates(): List<T>
/**
* @return Current state by [context]
*/
suspend fun getContextState(context: Any): State?
suspend fun getContextState(context: Any): T?
/**
* @return Current state by [context]
@ -43,21 +43,21 @@ interface DefaultStatesManagerRepo {
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
* new state by using [endChain] with that state
*/
class DefaultStatesManager(
private val repo: DefaultStatesManagerRepo = InMemoryDefaultStatesManagerRepo(),
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
) : StatesManager {
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
private val _onStartChain = MutableSharedFlow<State>(0)
override val onStartChain: Flow<State> = _onStartChain.asSharedFlow()
private val _onEndChain = MutableSharedFlow<State>(0)
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
class DefaultStatesManager<T : State>(
private val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
private val onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
) : StatesManager<T> {
private val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
private val _onStartChain = MutableSharedFlow<T>(0)
override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
private val _onEndChain = MutableSharedFlow<T>(0)
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
private val mapMutex = Mutex()
override suspend fun update(old: State, new: State) = mapMutex.withLock {
val stateByOldContext: State? = repo.getContextState(old.context)
override suspend fun update(old: T, new: T) = mapMutex.withLock {
val stateByOldContext: T? = repo.getContextState(old.context)
when {
stateByOldContext != old -> return@withLock
stateByOldContext == null || old.context == new.context -> {
@ -76,26 +76,26 @@ class DefaultStatesManager(
}
}
override suspend fun startChain(state: State) = mapMutex.withLock {
override suspend fun startChain(state: T) = mapMutex.withLock {
if (!repo.contains(state.context)) {
repo.set(state)
_onStartChain.emit(state)
}
}
private suspend fun endChainWithoutLock(state: State) {
private suspend fun endChainWithoutLock(state: T) {
if (repo.getContextState(state.context) == state) {
repo.removeState(state)
_onEndChain.emit(state)
}
}
override suspend fun endChain(state: State) {
override suspend fun endChain(state: T) {
mapMutex.withLock {
endChainWithoutLock(state)
}
}
override suspend fun getActiveStates(): List<State> = repo.getStates()
override suspend fun getActiveStates(): List<T> = repo.getStates()
}

View File

@ -6,20 +6,20 @@ import dev.inmo.micro_utils.fsm.common.State
* Simple [DefaultStatesManagerRepo] for [DefaultStatesManager] which will store data in [map] and use primitive
* functionality
*/
class InMemoryDefaultStatesManagerRepo(
private val map: MutableMap<Any, State> = mutableMapOf()
) : DefaultStatesManagerRepo {
override suspend fun set(state: State) {
class InMemoryDefaultStatesManagerRepo<T : State>(
private val map: MutableMap<Any, T> = mutableMapOf()
) : DefaultStatesManagerRepo<T> {
override suspend fun set(state: T) {
map[state.context] = state
}
override suspend fun removeState(state: State) {
override suspend fun removeState(state: T) {
map.remove(state.context)
}
override suspend fun getStates(): List<State> = map.values.toList()
override suspend fun getStates(): List<T> = map.values.toList()
override suspend fun getContextState(context: Any): State? = map[context]
override suspend fun getContextState(context: Any): T? = map[context]
override suspend fun contains(context: Any): Boolean = map.contains(context)
}

View File

@ -7,62 +7,13 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* Creates [DefaultStatesManager] with [InMemoryDefaultStatesManagerRepo]
*
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
* new state by using [endChain] with that state
*/
class InMemoryStatesManager(
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
) : StatesManager {
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
private val _onStartChain = MutableSharedFlow<State>(0)
override val onStartChain: Flow<State> = _onStartChain.asSharedFlow()
private val _onEndChain = MutableSharedFlow<State>(0)
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
private val contextsToStates = mutableMapOf<Any, State>()
private val mapMutex = Mutex()
override suspend fun update(old: State, new: State) = mapMutex.withLock {
when {
contextsToStates[old.context] != old -> return@withLock
old.context == new.context || !contextsToStates.containsKey(new.context) -> {
contextsToStates[old.context] = new
_onChainStateUpdated.emit(old to new)
}
else -> {
val stateOnNewOneContext = contextsToStates.getValue(new.context)
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
endChainWithoutLock(stateOnNewOneContext)
contextsToStates.remove(old.context)
contextsToStates[new.context] = new
_onChainStateUpdated.emit(old to new)
}
}
}
}
override suspend fun startChain(state: State) = mapMutex.withLock {
if (!contextsToStates.containsKey(state.context)) {
contextsToStates[state.context] = state
_onStartChain.emit(state)
}
}
private suspend fun endChainWithoutLock(state: State) {
if (contextsToStates[state.context] == state) {
contextsToStates.remove(state.context)
_onEndChain.emit(state)
}
}
override suspend fun endChain(state: State) {
mapMutex.withLock {
endChainWithoutLock(state)
}
}
override suspend fun getActiveStates(): List<State> = contextsToStates.values.toList()
}
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
fun <T: State> InMemoryStatesManager(
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
) = DefaultStatesManager(onContextsConflictResolver = onContextsConflictResolver)

View File

@ -1,6 +1,7 @@
import dev.inmo.micro_utils.fsm.common.*
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM
import dev.inmo.micro_utils.fsm.common.dsl.strictlyOn
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
import dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager
import kotlinx.coroutines.*
sealed interface TrafficLightState : State {
@ -25,9 +26,9 @@ class PlayableMain {
}
}
val statesManager = InMemoryStatesManager()
val statesManager = DefaultStatesManager<TrafficLightState>()
val machine = buildFSM {
val machine = buildFSM<TrafficLightState> {
strictlyOn<GreenCommon> {
delay(1000L)
YellowCommon(it.context).also(::println)

View File

@ -5,21 +5,21 @@ import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.pagination.getAll
class KeyValueBasedDefaultStatesManagerRepo(
private val keyValueRepo: KeyValueRepo<Any, State>
) : DefaultStatesManagerRepo {
override suspend fun set(state: State) {
class KeyValueBasedDefaultStatesManagerRepo<T : State>(
private val keyValueRepo: KeyValueRepo<Any, T>
) : DefaultStatesManagerRepo<T> {
override suspend fun set(state: T) {
keyValueRepo.set(state.context, state)
}
override suspend fun removeState(state: State) {
override suspend fun removeState(state: T) {
if (keyValueRepo.get(state.context) == state) {
keyValueRepo.unset(state.context)
}
}
override suspend fun getStates(): List<State> = keyValueRepo.getAll { keys(it) }.map { it.second }
override suspend fun getContextState(context: Any): State? = keyValueRepo.get(context)
override suspend fun getStates(): List<T> = keyValueRepo.getAll { keys(it) }.map { it.second }
override suspend fun getContextState(context: Any): T? = keyValueRepo.get(context)
override suspend fun contains(context: Any): Boolean = keyValueRepo.contains(context)
}

View File

@ -1,84 +0,0 @@
package dev.inmo.micro_utils.fsm.repos.common
import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.micro_utils.fsm.common.StatesManager
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.mappers.withMapper
import dev.inmo.micro_utils.repos.pagination.getAll
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@Deprecated("Replace with DefaultStatesManager and KeyValueBasedDefaultStatesManagerRepo")
class KeyValueBasedStatesManager(
private val keyValueRepo: KeyValueRepo<Any, State>,
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
) : StatesManager {
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
private val _onEndChain = MutableSharedFlow<State>(0)
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
override val onStartChain: Flow<State> = keyValueRepo.onNewValue.map { it.second }
private val mutex = Mutex()
override suspend fun update(old: State, new: State) {
mutex.withLock {
when {
keyValueRepo.get(old.context) != old -> return@withLock
old.context == new.context || !keyValueRepo.contains(new.context) -> {
keyValueRepo.set(old.context, new)
_onChainStateUpdated.emit(old to new)
}
else -> {
val stateOnNewOneContext = keyValueRepo.get(new.context)!!
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
endChainWithoutLock(stateOnNewOneContext)
keyValueRepo.unset(old.context)
keyValueRepo.set(new.context, new)
_onChainStateUpdated.emit(old to new)
}
}
}
}
}
override suspend fun startChain(state: State) {
if (!keyValueRepo.contains(state.context)) {
keyValueRepo.set(state.context, state)
}
}
private suspend fun endChainWithoutLock(state: State) {
if (keyValueRepo.get(state.context) == state) {
keyValueRepo.unset(state.context)
_onEndChain.emit(state)
}
}
override suspend fun endChain(state: State) {
mutex.withLock { endChainWithoutLock(state) }
}
override suspend fun getActiveStates(): List<State> {
return keyValueRepo.getAll { keys(it) }.map { it.second }
}
}
inline fun <reified TargetContextType, reified TargetStateType> createStatesManager(
targetKeyValueRepo: KeyValueRepo<TargetContextType, TargetStateType>,
noinline contextToOutTransformer: suspend Any.() -> TargetContextType,
noinline stateToOutTransformer: suspend State.() -> TargetStateType,
noinline outToContextTransformer: suspend TargetContextType.() -> Any,
noinline outToStateTransformer: suspend TargetStateType.() -> State,
) = KeyValueBasedStatesManager(
targetKeyValueRepo.withMapper<Any, State, TargetContextType, TargetStateType>(
contextToOutTransformer,
stateToOutTransformer,
outToContextTransformer,
outToStateTransformer
)
)

View File

@ -45,5 +45,5 @@ dokka_version=1.5.31
# Project data
group=dev.inmo
version=0.7.5
android_code_version=79
version=0.8.0
android_code_version=80