Skip to content

Commit

Permalink
Move configuration flags from createStateMachine overloads to Creatio…
Browse files Browse the repository at this point in the history
…nArguments structure (breaking change)
  • Loading branch information
nsk90 committed Mar 6, 2024
1 parent 4010b4c commit cb9524b
Show file tree
Hide file tree
Showing 17 changed files with 185 additions and 123 deletions.
7 changes: 4 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ val machine = createStateMachine(
```

By default, factory functions start state machine. You can control it using `start` argument.
All overloads accept optional argument `CreationArguments` which allows to change some options.

Subsequent samples will use `createStateMachine()` function, but you can choose that one which fits your needs.

Expand Down Expand Up @@ -422,8 +423,8 @@ like this `machine.processEvent(UndoEvent)`. State Machine will roll back last t
to previous state (except target-less transitions).
This API might be called as many times as needed.
To implement this feature library stores transitions in a stack, it takes memory,
so this feature is disabled by default and must be enabled explicitly using `createStateMachine(enableUndo = true)`
argument.
so this feature is disabled by default and must be enabled explicitly using
`createStateMachine(creationArguments = CreationArguments(isUndoEnabled = true))` argument.

Undo functionality is implemented as `Event`, so it possible to call `undo()` from notification callbacks, if you use
`QueuePendingEventHandler` (which is default) or its analog.
Expand Down Expand Up @@ -1065,7 +1066,7 @@ block to have a valid state references for transitions.
Keep in mind that states are mutated by machine instance, defining them with `object` keyword (i.e. singleton) often
makes your states live longer than machine. It is common use case when you have multiple similar machines
that are using same singleton states sequentially. Library detects such cases automatically by default
(see `autoDestroyOnStatesReuse` argument of `createStateMachine` function) and cleans states allowing for future reuse.
(see `autoDestroyOnStatesReuse` argument of `CreationArguments` structure) and cleans states allowing for future reuse.
You can disable automatic machine destruction on state reuse, and call `StateMachine.destroy()` manually if required,
or just do not use `object` keyword for defining states.
If you have your own `DefaultState` subclasses that are singletons and has data fields, use
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,20 @@ suspend fun createStateMachine(
name: String? = null,
childMode: ChildMode = ChildMode.EXCLUSIVE,
start: Boolean = true,
autoDestroyOnStatesReuse: Boolean = true,
enableUndo: Boolean = false,
doNotThrowOnMultipleTransitionsMatch: Boolean = false,
creationArguments: StateMachine.CreationArguments = StateMachine.CreationArguments(),
init: suspend BuildingStateMachine.() -> Unit
) = CoroutinesLibCoroutineAbstraction(scope).createStateMachine(
name,
childMode,
start,
autoDestroyOnStatesReuse,
enableUndo,
doNotThrowOnMultipleTransitionsMatch,
init
)
) = CoroutinesLibCoroutineAbstraction(scope)
.createStateMachine(name, childMode, start, creationArguments, init)

fun createStateMachineBlocking(
scope: CoroutineScope,
name: String? = null,
childMode: ChildMode = ChildMode.EXCLUSIVE,
start: Boolean = true,
autoDestroyOnStatesReuse: Boolean = true,
enableUndo: Boolean = false,
doNotThrowOnMultipleTransitionsMatch: Boolean = false,
creationArguments: StateMachine.CreationArguments = StateMachine.CreationArguments(),
init: suspend BuildingStateMachine.() -> Unit
) = with(CoroutinesLibCoroutineAbstraction(scope)) {
runBlocking {
createStateMachine(
name,
childMode,
start,
autoDestroyOnStatesReuse,
enableUndo,
doNotThrowOnMultipleTransitionsMatch,
init
)
createStateMachine(name, childMode, start, creationArguments, init)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,12 @@ suspend fun CoroutineAbstraction.createStateMachine(
name: String?,
childMode: ChildMode,
start: Boolean,
autoDestroyOnStatesReuse: Boolean,
enableUndo: Boolean,
doNotThrowOnMultipleTransitionsMatch: Boolean,
creationArguments: StateMachine.CreationArguments = StateMachine.CreationArguments(),
init: suspend BuildingStateMachine.() -> Unit
): StateMachine = StateMachineImpl(
name,
childMode,
autoDestroyOnStatesReuse,
enableUndo,
doNotThrowOnMultipleTransitionsMatch,
creationArguments,
this,
).apply {
init()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ open class BaseStateImpl(

private fun onStateReuseDetected() {
val machine = machine
if (machine.autoDestroyOnStatesReuse)
if (machine.creationArguments.autoDestroyOnStatesReuse)
machine.destroyBlocking()
else
error("State $this is already used in another machine instance")
Expand Down Expand Up @@ -173,7 +173,7 @@ open class BaseStateImpl(
.filter { it !is StateMachine } // exclude nested machines
.mapNotNull { it.recursiveFindUniqueResolvedTransition(eventAndArgument) }
.ifEmpty { listOfNotNull(findUniqueResolvedTransition(eventAndArgument)) } // allow transition override
return if (!machine.doNotThrowOnMultipleTransitionsMatch) {
return if (!machine.creationArguments.doNotThrowOnMultipleTransitionsMatch) {
check(resolvedTransitions.size <= 1) {
"Multiple transitions match ${eventAndArgument.event}, $transitions in $this"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ internal suspend fun <E : Event> InternalState.findUniqueResolvedTransition(even
val transitions = findTransitionsByEvent(eventAndArgument.event)
.map { it to it.produceTargetStateDirection(policy) }
.filter { it.second !is NoTransition }
return if (!machine.doNotThrowOnMultipleTransitionsMatch) {
return if (!machine.creationArguments.doNotThrowOnMultipleTransitionsMatch) {
check(transitions.size <= 1) { "Multiple transitions match ${eventAndArgument.event}, $transitions in $this" }
transitions.singleOrNull()
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ interface StateMachine : State {
val ignoredEventHandler: IgnoredEventHandler
val pendingEventHandler: PendingEventHandler

/**
* Configuration arguments which were used to create the machine
*/
val creationArguments: CreationArguments

/**
* If machine catches exception from client code (listeners callbacks) it stores it until event processing
* completes, and passes it to this handler. That keeps machine in well-defined predictable state and allows
Expand All @@ -38,24 +43,8 @@ interface StateMachine : State {
val isRunning: Boolean
val machineListeners: Collection<Listener>

/**
* Allows the library to automatically call destroy() on current state owning machine instance if user tries
* to reuse its states in another machine. Usually this is a result of using object states in sequentially created
* similar machines. destroy() will be called on the previous machine instance.
* If set to false an exception will be thrown on state reuse attempt.
*/
val autoDestroyOnStatesReuse: Boolean
val isDestroyed: Boolean

val isUndoEnabled: Boolean

/**
* If set to true, when multiple transitions match event the first matching transition is selected.
* if set to false, when multiple transitions match event exception is thrown.
* Default if false.
*/
val doNotThrowOnMultipleTransitionsMatch: Boolean

val coroutineAbstraction: CoroutineAbstraction

fun <L : Listener> addListener(listener: L): L
Expand Down Expand Up @@ -138,6 +127,23 @@ interface StateMachine : State {
fun interface ListenerExceptionHandler {
suspend fun onException(exception: Exception)
}

data class CreationArguments(
/**
* Allows the library to automatically call destroy() on current state owning machine instance if user tries
* to reuse its states in another machine. Usually this is a result of using object states in sequentially created
* similar machines. destroy() will be called on the previous machine instance.
* If set to false an exception will be thrown on state reuse attempt.
*/
val autoDestroyOnStatesReuse: Boolean = true,
val isUndoEnabled: Boolean = false,
/**
* If set to true, when multiple transitions match event the first matching transition is selected.
* if set to false, when multiple transitions match event exception is thrown.
* Default if false.
*/
val doNotThrowOnMultipleTransitionsMatch: Boolean = false,
)
}

fun StateMachine.startBlocking(argument: Any? = null) = coroutineAbstraction.runBlocking { start(argument) }
Expand Down Expand Up @@ -165,8 +171,10 @@ fun StateMachine.restartBlocking(argument: Any? = null) = coroutineAbstraction.r
* This function has same effect as alternative syntax processEvent(UndoEvent), but throws if undo feature is not enabled.
*/
suspend fun StateMachine.undo(argument: Any? = null): ProcessingResult = coroutineAbstraction.withContext {
check(isUndoEnabled) {
"Undo functionality is not enabled, use createStateMachine(enableUndo = true) argument to enable it."
check(creationArguments.isUndoEnabled) {
"Undo functionality is not enabled, " +
"use createStateMachine(creationArguments = CreationArguments(isUndoEnabled = true)) " +
"argument to enable it."
}
return@withContext processEvent(UndoEvent, argument)
}
Expand Down Expand Up @@ -223,22 +231,12 @@ fun createStdLibStateMachine(
name: String? = null,
childMode: ChildMode = ChildMode.EXCLUSIVE,
start: Boolean = true,
autoDestroyOnStatesReuse: Boolean = true,
enableUndo: Boolean = false,
doNotThrowOnMultipleTransitionsMatch: Boolean = false,
creationArguments: StateMachine.CreationArguments = StateMachine.CreationArguments(),
init: suspend BuildingStateMachine.() -> Unit
): StateMachine {
return with(StdLibCoroutineAbstraction()) {
runBlocking {
createStateMachine(
name,
childMode,
start,
autoDestroyOnStatesReuse,
enableUndo,
doNotThrowOnMultipleTransitionsMatch,
init
)
createStateMachine(name, childMode, start, creationArguments, init)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package ru.nsk.kstatemachine.statemachine

import ru.nsk.kstatemachine.*
import ru.nsk.kstatemachine.coroutines.CoroutineAbstraction
import ru.nsk.kstatemachine.event.*
import ru.nsk.kstatemachine.isSubStateOf
import ru.nsk.kstatemachine.state.*
import ru.nsk.kstatemachine.state.pseudo.UndoState
import ru.nsk.kstatemachine.transition.*
import ru.nsk.kstatemachine.transition.TransitionDirectionProducerPolicy.DefaultPolicy
import ru.nsk.kstatemachine.visitors.CheckUniqueNamesVisitor
import ru.nsk.kstatemachine.visitors.CleanupVisitor
import kotlin.reflect.KClass

/**
* Defines state machine API for internal library usage.
Expand All @@ -23,17 +24,31 @@ internal abstract class InternalStateMachine(name: String?, childMode: ChildMode
internal class StateMachineImpl(
name: String?,
childMode: ChildMode,
override val autoDestroyOnStatesReuse: Boolean,
override val isUndoEnabled: Boolean,
override val doNotThrowOnMultipleTransitionsMatch: Boolean,
override val creationArguments: StateMachine.CreationArguments,
override val coroutineAbstraction: CoroutineAbstraction,
) : InternalStateMachine(name, childMode) {
private val _machineListeners = mutableSetOf<StateMachine.Listener>()
override val machineListeners: Collection<StateMachine.Listener> get() = _machineListeners
override var logger: StateMachine.Logger = StateMachine.Logger {}
set(value) {
checkPropertyNotMutedOnRunningMachine(StateMachine.Logger::class)
field = value
}
override var ignoredEventHandler = StateMachine.IgnoredEventHandler {}
set(value) {
checkPropertyNotMutedOnRunningMachine(StateMachine.IgnoredEventHandler::class)
field = value
}
override var pendingEventHandler: StateMachine.PendingEventHandler = queuePendingEventHandler()
set(value) {
checkPropertyNotMutedOnRunningMachine(StateMachine.PendingEventHandler::class)
field = value
}
override var listenerExceptionHandler = StateMachine.ListenerExceptionHandler { throw it }
set(value) {
checkPropertyNotMutedOnRunningMachine(StateMachine.ListenerExceptionHandler::class)
field = value
}
private var _isDestroyed: Boolean = false
override val isDestroyed get() = _isDestroyed

Expand Down Expand Up @@ -63,7 +78,7 @@ internal class StateMachineImpl(
}
}
}
if (isUndoEnabled) {
if (creationArguments.isUndoEnabled) {
val undoState = addState(UndoState())
transition<WrappedEvent>("undo transition", undoState)
}
Expand Down Expand Up @@ -152,7 +167,7 @@ internal class StateMachineImpl(
}

private fun EventAndArgument<*>.wrap(): EventAndArgument<*> {
return if (isUndoEnabled && event is UndoEvent) {
return if (creationArguments.isUndoEnabled && event is UndoEvent) {
val wrapped = requireState<UndoState>().makeWrappedEvent()
EventAndArgument(wrapped, argument)
} else {
Expand Down Expand Up @@ -327,4 +342,7 @@ internal suspend inline fun <reified E : StartEvent> makeStartTransitionParams(
event,
argument,
)
}
}

private fun StateMachine.checkPropertyNotMutedOnRunningMachine(propertyType: KClass<*>) =
check(!isRunning) { "Can not change ${propertyType.simpleName} after state machine started" }
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,33 @@ package ru.nsk.kstatemachine.visitors

import ru.nsk.kstatemachine.event.Event
import ru.nsk.kstatemachine.state.DataState
import ru.nsk.kstatemachine.state.DefaultDataState
import ru.nsk.kstatemachine.state.HistoryState
import ru.nsk.kstatemachine.state.IState
import ru.nsk.kstatemachine.statemachine.StateMachine
import ru.nsk.kstatemachine.transition.Transition

/**
* This visitor collects structural information about [StateMachine] in order to compare two [StateMachine]
* instances for structural equality
* instances for structural equality.
* There is no guaranty that his class will find the difference in two really different machines,
* but it tries to do its best.
*/
internal class GetStructureHashCodeVisitor : RecursiveVisitor {
private val nodes = mutableListOf<String>()
val structureHashCode get() = nodes.hashCode()
private val records = mutableListOf<String>()
val structureHashCode get() = records.hashCode()

override fun visit(machine: StateMachine) {
nodes += machine.stateInfo()
records += machine.stateInfo()
records += "StateMachine creationArguments:${machine.creationArguments}"
machine.visitChildren()
}

override fun visit(state: IState) {
if (state !is StateMachine) { // do not check nested machines
nodes += state.stateInfo()
records += state.stateInfo()
state.visitChildren()
} else {
nodes += "class:${state::class.simpleName}, name:${state.name}"
records += "class:${state::class.simpleName}, name:${state.name}"
}
}

Expand All @@ -36,10 +38,10 @@ internal class GetStructureHashCodeVisitor : RecursiveVisitor {
"transitionsCount:${transitions.size}, " +
"childMode:$childMode" +
if (this is HistoryState) ", historyType:$historyType" else "" +
if (this is DataState<*>) ", dataClass:$dataClass, defaultData:$defaultData" else ""
if (this is DataState<*>) ", dataClass:$dataClass, defaultData:$defaultData" else ""

override fun <E : Event> visit(transition: Transition<E>) {
nodes += "class:${transition::class.simpleName}, " +
records += "class:${transition::class.simpleName}, " +
"name:${transition.name}, " +
"type:${transition.type}, " +
"event:${transition.eventMatcher.eventClass}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ru.nsk.samples

import kotlinx.coroutines.runBlocking
import ru.nsk.kstatemachine.statemachine.StateMachine
import ru.nsk.kstatemachine.event.Event
import ru.nsk.kstatemachine.state.*
import ru.nsk.kstatemachine.statemachine.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ru.nsk.kstatemachine.event.Event
import ru.nsk.kstatemachine.event.UndoEvent
import ru.nsk.kstatemachine.state.*
import ru.nsk.kstatemachine.statemachine.StateMachine
import ru.nsk.kstatemachine.statemachine.StateMachine.CreationArguments
import ru.nsk.kstatemachine.statemachine.createStateMachine
import ru.nsk.kstatemachine.statemachine.undo
import ru.nsk.kstatemachine.transition.unwrappedEvent
Expand All @@ -22,7 +23,10 @@ fun main() = runBlocking {
lateinit var state2: State
lateinit var state3: State

val machine = createStateMachine(this, enableUndo = true) {
val machine = createStateMachine(
this,
creationArguments = CreationArguments(isUndoEnabled = true)
) {
state1 = initialState("state1") {
transitionOn<SwitchEvent> { targetState = { state2 } }
}
Expand Down
Loading

0 comments on commit cb9524b

Please sign in to comment.