Skip to content

Commit

Permalink
Merge pull request #90 from ayanyev/safe-naming-for-uml
Browse files Browse the repository at this point in the history
Safe state and transition naming for uml
  • Loading branch information
nsk90 authored Feb 16, 2024
2 parents 2709cb6 + d4d7ade commit f2e376b
Show file tree
Hide file tree
Showing 23 changed files with 175 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import ru.nsk.kstatemachine.TransitionType.EXTERNAL
/**
* Base [IState] implementation for all states
*/
open class BaseStateImpl(override val name: String?, override val childMode: ChildMode) : InternalState() {
open class BaseStateImpl(
override val name: String?,
override val childMode: ChildMode,
override var metaInfo: MetaInfo? = null
) : InternalState() {

private class Data {
val listeners = mutableSetOf<IState.Listener>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ open class DefaultDataState<D : Any>(
override val defaultData: D? = null,
childMode: ChildMode = EXCLUSIVE,
private val dataExtractor: DataExtractor<D>,
) : BaseStateImpl(name, childMode), DataState<D> {
metaInfo: MetaInfo? = null,
) : BaseStateImpl(name, childMode, metaInfo), DataState<D> {
private var _data: D? = null
override val data: D get() = checkNotNull(_data) { "Data is not set. Is $this state active?" }

Expand Down Expand Up @@ -79,5 +80,6 @@ inline fun <reified D : Any> defaultFinalDataState(
open class DefaultFinalDataState<D : Any>(
name: String? = null,
defaultData: D? = null,
dataExtractor: DataExtractor<D>
) : DefaultDataState<D>(name, defaultData, EXCLUSIVE, dataExtractor), FinalDataState<D>
dataExtractor: DataExtractor<D>,
metaInfo: MetaInfo? = null
) : DefaultDataState<D>(name, defaultData, EXCLUSIVE, dataExtractor, metaInfo), FinalDataState<D>
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ package ru.nsk.kstatemachine
open class DefaultHistoryState(
name: String? = null,
private var _defaultState: IState? = null,
final override val historyType: HistoryType = HistoryType.SHALLOW
) : BasePseudoState(name), HistoryState {
final override val historyType: HistoryType = HistoryType.SHALLOW,
metaInfo: MetaInfo? = null
) : BasePseudoState(name, metaInfo), HistoryState {
override val defaultState get() = checkNotNull(_defaultState) { "Internal error, default state is not set" }

private var _storedState: IState? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@ import ru.nsk.kstatemachine.TransitionDirectionProducerPolicy.*
/**
* The most common state
*/
open class DefaultState(name: String? = null, childMode: ChildMode = EXCLUSIVE) :
BaseStateImpl(name, childMode), State
open class DefaultState(
name: String? = null,
childMode: ChildMode = EXCLUSIVE,
metaInfo: MetaInfo? = null
) : BaseStateImpl(name, childMode, metaInfo), State

open class DefaultFinalState(name: String? = null) : DefaultState(name), FinalState
open class DefaultFinalState(
name: String? = null,
metaInfo: MetaInfo? = null
) : DefaultState(name, metaInfo = metaInfo), FinalState

open class DefaultChoiceState(
name: String? = null,
metaInfo: MetaInfo? = null,
private val choiceAction: suspend EventAndArgument<*>.() -> State
) : BasePseudoState(name), RedirectPseudoState {
) : BasePseudoState(name, metaInfo), RedirectPseudoState {

override suspend fun resolveTargetState(policy: TransitionDirectionProducerPolicy<*>): TransitionDirection {
return internalResolveTargetState(policy, choiceAction)
Expand All @@ -23,8 +30,9 @@ open class DefaultChoiceState(

open class DefaultChoiceDataState<D : Any>(
name: String? = null,
metaInfo: MetaInfo? = null,
private val choiceAction: suspend EventAndArgument<*>.() -> DataState<D>,
) : DataState<D>, BasePseudoState(name), RedirectPseudoState {
) : DataState<D>, BasePseudoState(name, metaInfo), RedirectPseudoState {

override suspend fun resolveTargetState(policy: TransitionDirectionProducerPolicy<*>): TransitionDirection {
return internalResolveTargetState(policy, choiceAction)
Expand All @@ -48,7 +56,7 @@ private suspend fun IState.internalResolveTargetState(
}
}

open class BasePseudoState(name: String?) : BaseStateImpl(name, EXCLUSIVE), PseudoState {
open class BasePseudoState(name: String?, metaInfo: MetaInfo?) : BaseStateImpl(name, EXCLUSIVE, metaInfo), PseudoState {
override suspend fun doEnter(transitionParams: TransitionParams<*>) = internalError()
override suspend fun doExit(transitionParams: TransitionParams<*>) = internalError()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ open class DefaultTransition<E : Event>(
override val eventMatcher: EventMatcher<E>,
override val type: TransitionType,
sourceState: IState,
override val metaInfo: MetaInfo?,
) : InternalTransition<E> {
constructor(
name: String?,
eventMatcher: EventMatcher<E>,
type: TransitionType,
sourceState: IState,
targetState: IState?
) : this(name, eventMatcher, type, sourceState) {
targetState: IState?,
metaInfo: MetaInfo?
) : this(name, eventMatcher, type, sourceState, metaInfo) {
targetStateDirectionProducer = { it.targetStateOrStay(targetState) }
}

Expand All @@ -21,8 +23,9 @@ open class DefaultTransition<E : Event>(
eventMatcher: EventMatcher<E>,
type: TransitionType,
sourceState: IState,
targetStateDirectionProducer: TransitionDirectionProducer<E>
) : this(name, eventMatcher, type, sourceState) {
targetStateDirectionProducer: TransitionDirectionProducer<E>,
metaInfo: MetaInfo?
) : this(name, eventMatcher, type, sourceState, metaInfo) {
this.targetStateDirectionProducer = targetStateDirectionProducer
}

Expand Down
20 changes: 13 additions & 7 deletions kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/IState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ interface IState : TransitionStateApi, VisitorAcceptor {
val listeners: Collection<Listener>
val childMode: ChildMode

var metaInfo: MetaInfo?

fun <L : Listener> addListener(listener: L): L
fun removeListener(listener: Listener)

Expand Down Expand Up @@ -275,24 +277,28 @@ inline fun <reified D : Any> IState.initialFinalDataState(
noinline init: StateBlock<FinalDataState<D>>? = null
) = addInitialState(defaultFinalDataState(name, defaultData, dataExtractor), init)

fun IState.choiceState(name: String? = null, choiceAction: suspend EventAndArgument<*>.() -> State) =
addState(DefaultChoiceState(name, choiceAction))
fun IState.choiceState(
name: String? = null,
choiceAction: suspend EventAndArgument<*>.() -> State
) = addState(DefaultChoiceState(name, choiceAction = choiceAction))

fun IState.initialChoiceState(name: String? = null, choiceAction: suspend EventAndArgument<*>.() -> State) =
addInitialState(DefaultChoiceState(name, choiceAction))
fun IState.initialChoiceState(
name: String? = null,
choiceAction: suspend EventAndArgument<*>.() -> State
) = addInitialState(DefaultChoiceState(name, choiceAction = choiceAction))

fun <D : Any> IState.choiceDataState(
name: String? = null,
choiceAction: suspend EventAndArgument<*>.() -> DataState<D>
) = addState(DefaultChoiceDataState(name, choiceAction))
) = addState(DefaultChoiceDataState(name, choiceAction = choiceAction))

fun <D : Any> IState.initialChoiceDataState(
name: String? = null,
choiceAction: suspend EventAndArgument<*>.() -> DataState<D>
) = addInitialState(DefaultChoiceDataState(name, choiceAction))
) = addInitialState(DefaultChoiceDataState(name, choiceAction = choiceAction))

fun IState.historyState(
name: String? = null,
defaultState: IState? = null,
historyType: HistoryType = HistoryType.SHALLOW
historyType: HistoryType = HistoryType.SHALLOW,
) = addState(DefaultHistoryState(name, defaultState, historyType))
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ru.nsk.kstatemachine

interface MetaInfo
interface UmlMetaInfo: MetaInfo {
val umlLabel: String
}
fun umlLabel(label: String) = object : UmlMetaInfo {
override val umlLabel = label
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal class StateMachineImpl(
override var listenerExceptionHandler = StateMachine.ListenerExceptionHandler { throw it }
private var _isDestroyed: Boolean = false
override val isDestroyed get() = _isDestroyed
override var metaInfo: MetaInfo? = null

init {
transitionConditionally<StartEvent>("start transition") {
Expand All @@ -48,7 +49,7 @@ internal class StateMachineImpl(
}
if (isUndoEnabled) {
val undoState = addState(UndoState())
transition<WrappedEvent>("undo transition", undoState)
transition<WrappedEvent>("undo transition", targetState = undoState)
}
}

Expand Down Expand Up @@ -312,6 +313,7 @@ internal suspend inline fun <reified E : StartEvent> makeStartTransitionParams(
TransitionType.LOCAL,
sourceState,
targetState,
null
)

return TransitionParams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ru.nsk.kstatemachine.visitors.VisitorAcceptor
*/
interface Transition<E : Event> : VisitorAcceptor {
val name: String?
val metaInfo: MetaInfo?
val eventMatcher: EventMatcher<E>
val sourceState: IState
val type: TransitionType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ abstract class TransitionBuilder<E : Event>(protected val name: String?, protect
val listeners = mutableListOf<Transition.Listener>()
lateinit var eventMatcher: EventMatcher<E>
var type = TransitionType.LOCAL
var metaInfo: MetaInfo? = null

abstract fun build(): Transition<E>
}
Expand All @@ -33,7 +34,7 @@ abstract class GuardedTransitionBuilder<E : Event, S : IState>(name: String?, so
}
}

val transition = DefaultTransition(name, eventMatcher, type, sourceState, direction)
val transition = DefaultTransition(name, eventMatcher, type, sourceState, direction, metaInfo)
listeners.forEach { transition.addListener(it) }
return transition
}
Expand All @@ -56,7 +57,7 @@ abstract class GuardedTransitionOnBuilder<E : Event, S : IState>(name: String?,
}
}

val transition = DefaultTransition(name, eventMatcher, type, sourceState, direction)
val transition = DefaultTransition(name, eventMatcher, type, sourceState, direction, metaInfo)
listeners.forEach { transition.addListener(it) }
return transition
}
Expand All @@ -75,7 +76,7 @@ class ConditionalTransitionBuilder<E : Event>(name: String?, sourceState: IState
}
}

val transition = DefaultTransition(name, eventMatcher, type, sourceState, direction)
val transition = DefaultTransition(name, eventMatcher, type, sourceState, direction, metaInfo)
listeners.forEach { transition.addListener(it) }
return transition
}
Expand Down Expand Up @@ -112,7 +113,7 @@ class DataGuardedTransitionBuilder<E : DataEvent<D>, D : Any>(name: String?, sou
}
}

val transition = DefaultTransition(name, eventMatcher, type, sourceState, direction)
val transition = DefaultTransition(name, eventMatcher, type, sourceState, direction, metaInfo)
listeners.forEach { transition.addListener(it) }
return transition
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,15 @@ inline fun <reified E : Event> TransitionStateApi.transition(
name: String? = null,
targetState: State? = null,
type: TransitionType = LOCAL,
): Transition<E> = addTransition(DefaultTransition(name, matcherForEvent(asState()), type, asState(), targetState))
metaInfo: MetaInfo? = null,
): Transition<E> = addTransition(DefaultTransition(
name,
matcherForEvent(asState()),
type,
asState(),
targetState,
metaInfo
))

/**
* Creates transition.
Expand Down Expand Up @@ -113,8 +121,9 @@ inline fun <reified E : DataEvent<D>, D : Any> TransitionStateApi.dataTransition
name: String? = null,
targetState: DataState<D>,
type: TransitionType = LOCAL,
metaInfo: MetaInfo? = null,
): Transition<E> {
return addTransition(DefaultTransition(name, matcherForEvent(asState()), type, asState(), targetState))
return addTransition(DefaultTransition(name, matcherForEvent(asState()), type, asState(), targetState, metaInfo))
}

/**
Expand All @@ -123,8 +132,9 @@ inline fun <reified E : DataEvent<D>, D : Any> TransitionStateApi.dataTransition
inline fun <reified E : DataEvent<D>, D : Any> DataTransitionStateApi<D>.dataTransition(
name: String? = null,
type: TransitionType = LOCAL,
metaInfo: MetaInfo? = null,
): Transition<E> {
return addTransition(DefaultTransition(name, matcherForEvent(asState()), type, asState(), null))
return addTransition(DefaultTransition(name, matcherForEvent(asState()), type, asState(), null, metaInfo))
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package ru.nsk.kstatemachine

private data class StateAndEvent(val state: IState, val eventAndArgument: EventAndArgument<*>)

internal class UndoState : BasePseudoState("undoState") {
internal class UndoState : BasePseudoState("undoState", null) {
private val stack = mutableListOf<StateAndEvent>()

override suspend fun recursiveAfterTransitionComplete(transitionParams: TransitionParams<*>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,25 +53,25 @@ internal class ExportPlantUmlVisitor(
is HistoryState, is UndoState -> return
is RedirectPseudoState -> {
val stateName = state.graphName()
line("state $stateName $CHOICE")
line("state $stateName $CHOICE ${state.displayName()}")
@Suppress("UNCHECKED_CAST")
val targetStates = state.resolveTargetState(makeDirectionProducerPolicy<Event>())
.targetStates as Set<InternalState>
targetStates.forEach { targetState ->
crossLevelTransitions += "$stateName --> ${targetState.targetGraphName()}"
}
}
else -> line("state ${state.graphName()}")
else -> line("state ${state.displayName()}")
}
} else {
if (state !is StateMachine) { // ignore composed machines
line("state ${state.graphName()} {")
line("state ${state.displayName()} {")
++indent
processStateBody(state)
--indent
line("}")
} else {
line("state ${state.graphName()}")
line("state ${state.displayName()}")
}
}
}
Expand Down Expand Up @@ -134,11 +134,11 @@ internal class ExportPlantUmlVisitor(
private fun line(text: String) = builder.appendLine(SINGLE_INDENT.repeat(indent) + text)

private fun transitionLabel(transition: Transition<*>): String {
val entries = listOfNotNull(
transition.name,
val text = listOfNotNull(
(transition.metaInfo as? UmlMetaInfo)?.umlLabel ?: transition.name,
transition.eventMatcher.eventClass.simpleName.takeIf { showEventLabels },
)
return label(entries.joinToString())
).joinToString()
return " : $text".takeIf { text.isNotBlank() } ?: ""
}

private companion object {
Expand All @@ -150,10 +150,16 @@ internal class ExportPlantUmlVisitor(
const val CHOICE = "<<choice>>"

fun IState.graphName(): String {
val name = name?.replace(" ", "_") ?: "State${hashCode()}"
val name = (name ?: "State${hashCode()}").replace(Regex("[ -]"), "_")
return if (this !is StateMachine) name else "${name}_StateMachine"
}

fun IState.displayName(): String {
return (this.metaInfo as? UmlMetaInfo)?.umlLabel?.let { label ->
graphName() + " as \"$label\""
} ?: graphName()
}

fun InternalState.targetGraphName(): String {
return if (this is HistoryState) {
val prefix = requireInternalParent().graphName()
Expand All @@ -165,8 +171,6 @@ internal class ExportPlantUmlVisitor(
graphName()
}
}

fun label(text: String?) = if (!text.isNullOrBlank()) " : $text" else ""
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ private object InheritTransitionsSample {
* Nested states allow grouping states and inherit their parent transitions
*/
fun main() = runBlocking {
val machine = createStateMachine(this, "Nested states") {
val machine = createStateMachine(this, name = "Nested states") {
logger = StateMachine.Logger { println(it()) }

val state2 = finalState("State2")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ private object MermaidExportSample {
}

fun main() = runBlocking {
val machine = createStateMachine(this, "Nested states") {
val machine = createStateMachine(this, name = "Nested states") {
val state1 = initialState("State1")
val state3 = finalState("State3")

Expand Down
Loading

0 comments on commit f2e376b

Please sign in to comment.