All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.toxicbakery.kfinstatemachine.StateMachine.kt Maven / Gradle / Ivy

The newest version!
package com.toxicbakery.kfinstatemachine

import kotlin.jvm.Volatile
import kotlin.reflect.KClass

/**
 * A basic state machine that is not thread safe.
 */
open class StateMachine : IStateMachine {

    private val transitionRules: Array>

    private val _transitionCallbacks: MutableList> = mutableListOf()

    /**
     * An immutable list of the currently registered callbacks.
     */
    val transitionCallbacks: List>
        get() = _transitionCallbacks

    @Volatile
    final override var state: S

    @Suppress("UNCHECKED_CAST")
    constructor(
        initialState: S,
        vararg transitionRules: TransitionDef
    ) {
        state = initialState
        this.transitionRules = transitionRules as Array>
    }

    constructor(
        initialState: S,
        transitions: List>
    ) {
        state = initialState
        transitionRules = transitions.toTypedArray()
    }

    @Suppress("UNCHECKED_CAST")
    override val transitions: Set>
        get() = transitionRules.filter { rule -> rule.oldState == state }
            .map { rule -> rule.transition as KClass }
            .toSet()

    override fun transition(transition: T) {
        val startState = state
        val edge = edge(transition)
        val callbacks = transitionCallbacks.toList()
        callbacks.forEach { callback ->
            callback.enteringState(this, startState, transition, edge.newState)
        }
        state = edge.newState
        callbacks.forEach { callback ->
            callback.enteredState(this, startState, transition, edge.newState)
        }
    }

    @Suppress("UNCHECKED_CAST")
    override fun transitionsTo(targetState: S): Set> =
        transitionRules
            .filter { rule: TransitionDef ->
                rule.oldState == state
                        && rule.newState == targetState
            }
            .map { it.transition as KClass }
            .toSet()

    /**
     * Register a callback for state transition updates.
     *
     * @param transitionCallback to be registered
     */
    fun registerCallback(transitionCallback: TransitionCallback) =
        _transitionCallbacks.add(transitionCallback)

    /**
     * Unregister a callback from state transition updates.
     *
     * @param transitionCallback to be unregistered
     */
    fun unregisterCallback(transitionCallback: TransitionCallback) =
        _transitionCallbacks.remove(transitionCallback)

    private fun edge(transition: Any): TransitionDef = transitionRules
        .filter { transitionRule ->
            transitionRule.oldState == state
                    && transitionRule.transition.isInstance(transition)
        }
        .let { transitions: List> ->
            when {
                transitions.isEmpty() ->
                    error("Invalid transition `$transition` for state `$state`.\nValid transitions ${this.transitions}")

                transitions.size > 1 ->
                    error("Ambiguous transition `$transition` for state `$state`.\nMatches ${transitions.toTransitionsString()}.")

                else -> transitions.first()
            }
        }

    companion object {
        private fun  List>.toTransitionsString(): String =
            joinToString(separator = "\n") { transitionRule ->
                "${transitionRule.oldState} -> ${transitionRule.newState}"
            }

        fun  transition(oldState: F, transition: KClass, newState: F): TransitionDef =
            TransitionDef(
                oldState = oldState,
                transition = transition,
                newState = newState
            )
    }

}

data class TransitionDef(
    val oldState: S,
    val transition: KClass,
    val newState: S
)

interface TransitionCallback {

    /**
     * After a state transition has been verified to be legal but has not yet been applied to the machine.
     *
     * @param stateMachine the machine notifying the state change
     * @param currentState the current state of the machine
     * @param transition the transition that initiated the state change
     * @param targetState the resulting state of this transition
     */
    fun enteringState(
        stateMachine: StateMachine,
        currentState: S,
        transition: T,
        targetState: S
    )

    /**
     * After a state transition has been verified to be legal and also applied to a machine.
     *
     * @param stateMachine the machine notifying the state change
     * @param previousState the previous state of the machine before the transition was applied
     * @param transition the transition that initiated the state change
     * @param currentState the resulting state of this transition
     */
    fun enteredState(
        stateMachine: StateMachine,
        previousState: S,
        transition: T,
        currentState: S
    )

}

/**
 * Create a transition builder for a given state and transition event.
 *
 * @param event that will trigger the transition
 */
infix fun  S.onTransition(event: KClass): TransitionBuilder =
    ConcreteTransitionBuilder(this, event)

/**
 * Define how a transition builder should end completing a fully defined transition definition.
 *
 * @param newState result of a successful transition
 */
infix fun  TransitionBuilder.resultsIn(newState: S): TransitionDef =
    TransitionDef(startState, event, newState)

/**
 * Base definition for a partially defined transition.
 */
interface TransitionBuilder {
    val startState: S
    val event: KClass
}

private class ConcreteTransitionBuilder(
    override val startState: S,
    override val event: KClass
) : TransitionBuilder




© 2015 - 2025 Weber Informatics LLC | Privacy Policy