Skip to content

Commit 9b94ea1

Browse files
authored
Merge pull request #119 from KStateMachine/mutabledatastate
Add MutableDataState
2 parents 02e7b07 + 7f75f2c commit 9b94ea1

File tree

13 files changed

+612
-121
lines changed

13 files changed

+612
-121
lines changed

docs/pages/transitions/typesafe_transitions.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ createStateMachine(scope) {
3737
is activated it requires data value from a `DataEvent`. You can use `lastData` field to access last data value even
3838
after state exit, it falls back to `defaultData` if provided or throws.
3939

40+
### MutableDataState
41+
42+
In some cases it might be necessary to change `DataState`'s data manually. To archive it the library introduces
43+
additional `MutableDataState` type, which allows `data` field mutation with `setData` method.
44+
This is more flexible but less strict approach than using simple `DataState` which allows `data` field update
45+
only by a transition.
46+
4047
### Target-less data transitions
4148

4249
You can define target-less transitions for `DataState`. Please, note that if you want such transition to change state's

kstatemachine/api/kstatemachine.api

Lines changed: 66 additions & 20 deletions
Large diffs are not rendered by default.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Author: Mikhail Fedotov
3+
* Github: https://github.com/KStateMachine
4+
* Copyright (c) 2024.
5+
* All rights reserved.
6+
*/
7+
8+
@file:OptIn(ExperimentalContracts::class)
9+
10+
package ru.nsk.kstatemachine.state
11+
12+
import ru.nsk.kstatemachine.event.DataExtractor
13+
import ru.nsk.kstatemachine.event.defaultDataExtractor
14+
import kotlin.contracts.ExperimentalContracts
15+
import kotlin.contracts.InvocationKind
16+
import kotlin.contracts.contract
17+
18+
inline fun <reified D : Any> IState.dataState(
19+
name: String? = null,
20+
defaultData: D? = null,
21+
childMode: ChildMode = ChildMode.EXCLUSIVE,
22+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
23+
): DataState<D> = addState(defaultDataState(name, defaultData, childMode, dataExtractor))
24+
25+
suspend inline fun <reified D : Any> IState.dataState(
26+
name: String? = null,
27+
defaultData: D? = null,
28+
childMode: ChildMode = ChildMode.EXCLUSIVE,
29+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
30+
init: StateBlock<DataState<D>>
31+
): DataState<D> {
32+
contract {
33+
callsInPlace(init, InvocationKind.EXACTLY_ONCE)
34+
}
35+
return addState(defaultDataState(name, defaultData, childMode, dataExtractor), init)
36+
}
37+
38+
/**
39+
* @param defaultData is necessary for initial [DataState]
40+
*/
41+
inline fun <reified D : Any> IState.initialDataState(
42+
name: String? = null,
43+
defaultData: D,
44+
childMode: ChildMode = ChildMode.EXCLUSIVE,
45+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
46+
): DataState<D> = addInitialState(defaultDataState(name, defaultData, childMode, dataExtractor))
47+
48+
/**
49+
* @param defaultData is necessary for initial [DataState]
50+
*/
51+
suspend inline fun <reified D : Any> IState.initialDataState(
52+
name: String? = null,
53+
defaultData: D,
54+
childMode: ChildMode = ChildMode.EXCLUSIVE,
55+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
56+
init: StateBlock<DataState<D>>
57+
): DataState<D> {
58+
contract {
59+
callsInPlace(init, InvocationKind.EXACTLY_ONCE)
60+
}
61+
return addInitialState(defaultDataState(name, defaultData, childMode, dataExtractor), init)
62+
}
63+
64+
inline fun <reified D : Any> IState.finalDataState(
65+
name: String? = null,
66+
defaultData: D? = null,
67+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
68+
): FinalDataState<D> = addFinalState(defaultFinalDataState(name, defaultData, dataExtractor))
69+
70+
suspend inline fun <reified D : Any> IState.finalDataState(
71+
name: String? = null,
72+
defaultData: D? = null,
73+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
74+
init: StateBlock<FinalDataState<D>>
75+
): FinalDataState<D> {
76+
contract {
77+
callsInPlace(init, InvocationKind.EXACTLY_ONCE)
78+
}
79+
return addFinalState(defaultFinalDataState(name, defaultData, dataExtractor), init)
80+
}
81+
82+
inline fun <reified D : Any> IState.initialFinalDataState(
83+
name: String? = null,
84+
defaultData: D? = null,
85+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
86+
): FinalDataState<D> = addInitialState(defaultFinalDataState(name, defaultData, dataExtractor))
87+
88+
suspend inline fun <reified D : Any> IState.initialFinalDataState(
89+
name: String? = null,
90+
defaultData: D? = null,
91+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
92+
init: StateBlock<FinalDataState<D>>
93+
): FinalDataState<D> {
94+
contract {
95+
callsInPlace(init, InvocationKind.EXACTLY_ONCE)
96+
}
97+
return addInitialState(defaultFinalDataState(name, defaultData, dataExtractor), init)
98+
}

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/state/DefaultDataState.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ open class DefaultDataState<D : Any>(
2626
childMode: ChildMode = EXCLUSIVE,
2727
private val dataExtractor: DataExtractor<D>,
2828
) : BaseStateImpl(name, childMode), DataState<D> {
29-
private var _data: D? = null
29+
protected var _data: D? = null
3030
override val data: D get() = checkNotNull(_data) { "Data is not set. Is $this state active?" }
3131

32-
private var _lastData: D? = null
32+
protected var _lastData: D? = null
3333
override val lastData: D
3434
get() = checkNotNull(_lastData ?: defaultData) {
3535
"Last data is not available yet in $this, and default data not provided"
@@ -60,10 +60,14 @@ open class DefaultDataState<D : Any>(
6060

6161
private fun assignData(event: Event) {
6262
@Suppress("UNCHECKED_CAST")
63-
event as DataEvent<D>
64-
with(event.data) {
65-
_data = this
66-
_lastData = this
63+
val dataEvent = event as? DataEvent<D>
64+
if (dataEvent != null && dataClass.isInstance(event.data)) {
65+
with(event.data) {
66+
_data = this
67+
_lastData = this
68+
}
69+
} else {
70+
_data = lastData
6771
}
6872
}
6973

@@ -85,7 +89,7 @@ inline fun <reified D : Any> defaultFinalDataState(
8589
name: String? = null,
8690
defaultData: D? = null,
8791
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
88-
): DefaultFinalDataState<D> = DefaultFinalDataState(name, defaultData, dataExtractor)
92+
) = DefaultFinalDataState(name, defaultData, dataExtractor)
8993

9094
open class DefaultFinalDataState<D : Any>(
9195
name: String? = null,

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/state/IState.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,26 +123,39 @@ interface DataState<D : Any> : IState, DataTransitionStateApi<D> {
123123
val defaultData: D?
124124

125125
/**
126-
* This property might be accessed only while this state is active
126+
* This property might be accessed only while this state is active, throws otherwise
127127
*/
128128
val data: D
129129

130130
/**
131131
* Similar to [data] but its value is not cleared when state finishes.
132-
* If state was not entered this property falls back to [defaultData] if it was specified
132+
* If state was not entered this property falls back to [defaultData] if it was specified or throws
133133
*/
134134
val lastData: D
135135

136136
val dataClass: KClass<D>
137137
}
138138

139+
/**
140+
* Mutable version of [DefaultDataState]. Allows to update [data] value manually using [setData] method.
141+
*/
142+
interface MutableDataState<D : Any> : DataState<D> {
143+
/**
144+
* Updates [data] and [lastData] values.
145+
* Note that [data] can be set only if the state is active.
146+
* If the state is not active only [lastData] will be updated.
147+
*/
148+
fun setData(data: D)
149+
}
150+
139151
/**
140152
* Marker interface. When [StateMachine] enters this state it finishes and does not accept events anymore.
141153
* It is possible to use this interface to mark final state directly instead of subclassing [DefaultFinalState]
142154
*/
143155
interface IFinalState : IState
144156
interface FinalState : IFinalState, State
145157
interface FinalDataState<D : Any> : IFinalState, DataState<D>
158+
interface FinalMutableDataState<D : Any> : IFinalState, MutableDataState<D>
146159

147160
/**
148161
* Pseudo state is a state that machine passes automatically without explicit event. It cannot be active.
@@ -241,4 +254,4 @@ inline fun <reified S : IState> IState.requireState(recursive: Boolean = true) =
241254

242255
suspend operator fun <S : IState> S.invoke(block: StateBlock<S>) = block()
243256

244-
fun IState.machineOrNull(): StateMachine? = this as? StateMachine ?: parent?.machineOrNull()
257+
fun IState.machineOrNull(): StateMachine? = this as? StateMachine ?: parent?.machineOrNull()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Author: Mikhail Fedotov
3+
* Github: https://github.com/KStateMachine
4+
* Copyright (c) 2024.
5+
* All rights reserved.
6+
*/
7+
8+
package ru.nsk.kstatemachine.state
9+
10+
import ru.nsk.kstatemachine.event.DataExtractor
11+
import ru.nsk.kstatemachine.event.defaultDataExtractor
12+
import ru.nsk.kstatemachine.state.ChildMode.EXCLUSIVE
13+
14+
/** inline constructor function */
15+
inline fun <reified D : Any> defaultMutableDataState(
16+
name: String? = null,
17+
defaultData: D? = null,
18+
childMode: ChildMode = EXCLUSIVE,
19+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
20+
) = DefaultMutableDataState(name, defaultData, childMode, dataExtractor)
21+
22+
open class DefaultMutableDataState<D : Any>(
23+
name: String? = null,
24+
defaultData: D? = null,
25+
childMode: ChildMode = EXCLUSIVE,
26+
dataExtractor: DataExtractor<D>,
27+
) : DefaultDataState<D>(name, defaultData, childMode, dataExtractor), MutableDataState<D> {
28+
override fun setData(data: D) {
29+
if (isActive) _data = data
30+
_lastData = data
31+
}
32+
}
33+
34+
/** inline constructor function */
35+
inline fun <reified D : Any> defaultFinalMutableDataState(
36+
name: String? = null,
37+
defaultData: D? = null,
38+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
39+
) = DefaultFinalMutableDataState(name, defaultData, dataExtractor)
40+
41+
open class DefaultFinalMutableDataState<D : Any>(
42+
name: String? = null,
43+
defaultData: D? = null,
44+
dataExtractor: DataExtractor<D>,
45+
) : DefaultMutableDataState<D>(name, defaultData, EXCLUSIVE, dataExtractor), FinalMutableDataState<D>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Author: Mikhail Fedotov
3+
* Github: https://github.com/KStateMachine
4+
* Copyright (c) 2024.
5+
* All rights reserved.
6+
*/
7+
8+
@file:OptIn(ExperimentalContracts::class)
9+
10+
package ru.nsk.kstatemachine.state
11+
12+
import ru.nsk.kstatemachine.event.DataExtractor
13+
import ru.nsk.kstatemachine.event.defaultDataExtractor
14+
import kotlin.contracts.ExperimentalContracts
15+
import kotlin.contracts.InvocationKind
16+
import kotlin.contracts.contract
17+
18+
inline fun <reified D : Any> IState.mutableDataState(
19+
name: String? = null,
20+
defaultData: D? = null,
21+
childMode: ChildMode = ChildMode.EXCLUSIVE,
22+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
23+
): MutableDataState<D> = addState(defaultMutableDataState(name, defaultData, childMode, dataExtractor))
24+
25+
suspend inline fun <reified D : Any> IState.mutableDataState(
26+
name: String? = null,
27+
defaultData: D? = null,
28+
childMode: ChildMode = ChildMode.EXCLUSIVE,
29+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
30+
init: StateBlock<MutableDataState<D>>
31+
): MutableDataState<D> {
32+
contract {
33+
callsInPlace(init, InvocationKind.EXACTLY_ONCE)
34+
}
35+
return addState(defaultMutableDataState(name, defaultData, childMode, dataExtractor), init)
36+
}
37+
38+
/**
39+
* @param defaultData is necessary for initial [DataState]
40+
*/
41+
inline fun <reified D : Any> IState.initialMutableDataState(
42+
name: String? = null,
43+
defaultData: D,
44+
childMode: ChildMode = ChildMode.EXCLUSIVE,
45+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
46+
): MutableDataState<D> = addInitialState(defaultMutableDataState(name, defaultData, childMode, dataExtractor))
47+
48+
/**
49+
* @param defaultData is necessary for initial [DataState]
50+
*/
51+
suspend inline fun <reified D : Any> IState.initialMutableDataState(
52+
name: String? = null,
53+
defaultData: D,
54+
childMode: ChildMode = ChildMode.EXCLUSIVE,
55+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
56+
init: StateBlock<MutableDataState<D>>
57+
): MutableDataState<D> {
58+
contract {
59+
callsInPlace(init, InvocationKind.EXACTLY_ONCE)
60+
}
61+
return addInitialState(defaultMutableDataState(name, defaultData, childMode, dataExtractor), init)
62+
}
63+
64+
inline fun <reified D : Any> IState.finalMutableDataState(
65+
name: String? = null,
66+
defaultData: D? = null,
67+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
68+
): FinalMutableDataState<D> = addFinalState(defaultFinalMutableDataState(name, defaultData, dataExtractor))
69+
70+
suspend inline fun <reified D : Any> IState.finalMutableDataState(
71+
name: String? = null,
72+
defaultData: D? = null,
73+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
74+
init: StateBlock<FinalMutableDataState<D>>
75+
): FinalMutableDataState<D> {
76+
contract {
77+
callsInPlace(init, InvocationKind.EXACTLY_ONCE)
78+
}
79+
return addFinalState(defaultFinalMutableDataState(name, defaultData, dataExtractor), init)
80+
}
81+
82+
inline fun <reified D : Any> IState.initialFinalMutableDataState(
83+
name: String? = null,
84+
defaultData: D? = null,
85+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
86+
): FinalMutableDataState<D> = addInitialState(defaultFinalMutableDataState(name, defaultData, dataExtractor))
87+
88+
suspend inline fun <reified D : Any> IState.initialFinalMutableDataState(
89+
name: String? = null,
90+
defaultData: D? = null,
91+
dataExtractor: DataExtractor<D> = defaultDataExtractor(),
92+
init: StateBlock<FinalMutableDataState<D>>
93+
): FinalMutableDataState<D> {
94+
contract {
95+
callsInPlace(init, InvocationKind.EXACTLY_ONCE)
96+
}
97+
return addInitialState(defaultFinalMutableDataState(name, defaultData, dataExtractor), init)
98+
}

0 commit comments

Comments
 (0)