Skip to content

Commit 15a011e

Browse files
committed
Add requireNonBlankNames option
It allows to force check that all states and transitions has filled names
1 parent cb9524b commit 15a011e

File tree

6 files changed

+169
-3
lines changed

6 files changed

+169
-3
lines changed

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/statemachine/StateMachine.kt

+5
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ interface StateMachine : State {
143143
* Default if false.
144144
*/
145145
val doNotThrowOnMultipleTransitionsMatch: Boolean = false,
146+
/**
147+
* If enabled, throws exception on the machine start,
148+
* if it contains states or transitions with null or blank names
149+
*/
150+
val requireNonBlankNames: Boolean = false,
146151
)
147152
}
148153

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/statemachine/StateMachineImpl.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package ru.nsk.kstatemachine.statemachine
22

3-
import ru.nsk.kstatemachine.*
43
import ru.nsk.kstatemachine.coroutines.CoroutineAbstraction
54
import ru.nsk.kstatemachine.event.*
5+
import ru.nsk.kstatemachine.isSubStateOf
66
import ru.nsk.kstatemachine.state.*
77
import ru.nsk.kstatemachine.state.pseudo.UndoState
88
import ru.nsk.kstatemachine.transition.*
99
import ru.nsk.kstatemachine.transition.TransitionDirectionProducerPolicy.DefaultPolicy
1010
import ru.nsk.kstatemachine.visitors.CheckUniqueNamesVisitor
1111
import ru.nsk.kstatemachine.visitors.CleanupVisitor
12+
import ru.nsk.kstatemachine.visitors.checkNonBlankNames
1213
import kotlin.reflect.KClass
1314

1415
/**
@@ -122,6 +123,8 @@ internal class StateMachineImpl(
122123

123124
private fun checkBeforeRunMachine() {
124125
accept(CheckUniqueNamesVisitor())
126+
if (creationArguments.requireNonBlankNames)
127+
checkNonBlankNames()
125128
checkNotDestroyed()
126129
check(!isRunning) { "$this is already started" }
127130
check(!isProcessingEvent) { "$this is already processing event, this is internal error, please report a bug" }

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/visitors/GetStructureHashCodeVisitor.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ru.nsk.kstatemachine.event.Event
44
import ru.nsk.kstatemachine.state.DataState
55
import ru.nsk.kstatemachine.state.HistoryState
66
import ru.nsk.kstatemachine.state.IState
7+
import ru.nsk.kstatemachine.statemachine.QueuePendingEventHandler
78
import ru.nsk.kstatemachine.statemachine.StateMachine
89
import ru.nsk.kstatemachine.transition.Transition
910

@@ -19,7 +20,9 @@ internal class GetStructureHashCodeVisitor : RecursiveVisitor {
1920

2021
override fun visit(machine: StateMachine) {
2122
records += machine.stateInfo()
22-
records += "StateMachine creationArguments:${machine.creationArguments}"
23+
records += "StateMachine creationArguments:${machine.creationArguments}, " +
24+
"is${QueuePendingEventHandler::class.simpleName}:" +
25+
"${machine.pendingEventHandler is QueuePendingEventHandler}"
2326
machine.visitChildren()
2427
}
2528

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package ru.nsk.kstatemachine.visitors
2+
3+
import ru.nsk.kstatemachine.event.Event
4+
import ru.nsk.kstatemachine.state.IState
5+
import ru.nsk.kstatemachine.statemachine.StateMachine
6+
import ru.nsk.kstatemachine.transition.Transition
7+
8+
/**
9+
* Checks that machine contains states and transitions with filled and non-blank names.
10+
*/
11+
internal class RequireNonBlankNamesVisitor : RecursiveVisitor {
12+
private val invalidStates = mutableSetOf<IState>()
13+
private val invalidTransitions = mutableSetOf<Transition<*>>()
14+
15+
override fun visit(machine: StateMachine) {
16+
if (machine.name.isNullOrBlank())
17+
invalidStates += machine
18+
machine.visitChildren()
19+
}
20+
21+
override fun visit(state: IState) {
22+
if (state.name.isNullOrBlank())
23+
invalidStates += state
24+
25+
if (state !is StateMachine) // do not check nested machines
26+
state.visitChildren()
27+
}
28+
29+
override fun <E : Event> visit(transition: Transition<E>) {
30+
if (transition.name.isNullOrBlank())
31+
invalidTransitions += transition
32+
}
33+
34+
fun hasBlankNames() = invalidStates.isNotEmpty() || invalidTransitions.isNotEmpty()
35+
36+
fun checkNonBlankNames() {
37+
check(!hasBlankNames()) {
38+
val statesText = invalidStates.joinToString { "$it (child of ${it.parent})" }
39+
val transitionsText = invalidTransitions.joinToString { "$it (in ${it.sourceState})" }
40+
"There were blank names in states: $statesText transitions: $transitionsText"
41+
}
42+
}
43+
}
44+
45+
fun StateMachine.hasBlankNames(): Boolean {
46+
val visitor = RequireNonBlankNamesVisitor()
47+
accept(visitor)
48+
return visitor.hasBlankNames()
49+
}
50+
51+
fun StateMachine.checkNonBlankNames() {
52+
val visitor = RequireNonBlankNamesVisitor()
53+
accept(visitor)
54+
visitor.checkNonBlankNames()
55+
}

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/visitors/Visitor.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import ru.nsk.kstatemachine.transition.Transition
77

88
/**
99
* Suspendable interface for visiting state machine components
10-
* Visitor must is used instead of extension functions to preserve virtual behaviour, which is missing with extensions.
10+
* Visitor must be used instead of extension functions to preserve virtual behaviour, which is missing with extensions.
1111
*/
1212
interface CoVisitor {
1313
suspend fun visit(machine: StateMachine)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package ru.nsk.kstatemachine.visitors
2+
3+
import io.kotest.assertions.throwables.shouldThrow
4+
import io.kotest.core.spec.style.StringSpec
5+
import io.kotest.matchers.shouldBe
6+
import ru.nsk.kstatemachine.CoroutineStarterType
7+
import ru.nsk.kstatemachine.SwitchEvent
8+
import ru.nsk.kstatemachine.createTestStateMachine
9+
import ru.nsk.kstatemachine.state.initialState
10+
import ru.nsk.kstatemachine.state.transition
11+
import ru.nsk.kstatemachine.statemachine.StateMachine.CreationArguments
12+
13+
class RequireNonBlankNamesVisitorTest : StringSpec({
14+
CoroutineStarterType.entries.forEach { coroutineStarterType ->
15+
"check machine with multiple blank names" {
16+
val machine = createTestStateMachine(coroutineStarterType) {
17+
initialState {
18+
transition<SwitchEvent>()
19+
}
20+
}
21+
22+
machine.hasBlankNames() shouldBe true
23+
shouldThrow<IllegalStateException> {
24+
machine.checkNonBlankNames()
25+
}
26+
}
27+
28+
"check machine blank name" {
29+
val machine = createTestStateMachine(coroutineStarterType) {
30+
initialState("initial") {
31+
transition<SwitchEvent>("transition")
32+
}
33+
}
34+
35+
machine.hasBlankNames() shouldBe true
36+
shouldThrow<IllegalStateException> {
37+
machine.checkNonBlankNames()
38+
}
39+
}
40+
41+
"check state blank name" {
42+
val machine = createTestStateMachine(coroutineStarterType, "machine") {
43+
initialState {
44+
transition<SwitchEvent>("transition")
45+
}
46+
}
47+
48+
machine.hasBlankNames() shouldBe true
49+
shouldThrow<IllegalStateException> {
50+
machine.checkNonBlankNames()
51+
}
52+
}
53+
54+
"check transition blank name" {
55+
val machine = createTestStateMachine(coroutineStarterType, "machine") {
56+
initialState("initial") {
57+
transition<SwitchEvent>()
58+
}
59+
}
60+
61+
machine.hasBlankNames() shouldBe true
62+
shouldThrow<IllegalStateException> {
63+
machine.checkNonBlankNames()
64+
}
65+
}
66+
67+
"check machine without blank names" {
68+
val machine = createTestStateMachine(coroutineStarterType, "machine") {
69+
initialState("initial") {
70+
transition<SwitchEvent>("transition")
71+
}
72+
}
73+
74+
machine.hasBlankNames() shouldBe false
75+
machine.checkNonBlankNames()
76+
}
77+
78+
"check machine started with blank names and disabled check" {
79+
createTestStateMachine(
80+
coroutineStarterType,
81+
creationArguments = CreationArguments(requireNonBlankNames = false)
82+
) {
83+
initialState()
84+
}
85+
}
86+
87+
"check exception thrown on machine start with blank names and enabled check" {
88+
val machine = createTestStateMachine(
89+
coroutineStarterType,
90+
start = false,
91+
creationArguments = CreationArguments(requireNonBlankNames = true)
92+
) {
93+
initialState()
94+
}
95+
shouldThrow<IllegalStateException> {
96+
machine.start()
97+
}
98+
}
99+
}
100+
})

0 commit comments

Comments
 (0)