Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 78653dc

Browse files
committedApr 7, 2024·
Rebuild the exemplar for the new api
1 parent 17711eb commit 78653dc

File tree

3 files changed

+250
-0
lines changed

3 files changed

+250
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package app.cash.kfsm.exemplar
2+
3+
import app.cash.kfsm.Value
4+
5+
data class Hamster(
6+
val name: String,
7+
override val state: State
8+
): Value<Hamster, Hamster.State> {
9+
override fun update(newState: State): Hamster = this.copy(state = newState)
10+
11+
fun eat(food: String) {
12+
println("@ (・ェ・´)◞ (eats $food)")
13+
}
14+
15+
fun sleep() {
16+
println("◟(`・ェ・) ╥━╥ (goes to bed)")
17+
}
18+
19+
sealed class State(to: () -> Set<State>) : app.cash.kfsm.State(to)
20+
21+
/** Hamster is awake... and hungry! */
22+
data object Awake : State({ setOf(Eating) })
23+
24+
/** Hamster is eating ... what will they do next? */
25+
data object Eating : State({ setOf(RunningOnWheel, Asleep, Resting) })
26+
27+
/** Wheeeeeee! */
28+
data object RunningOnWheel : State({ setOf(Asleep, Resting) })
29+
30+
/** Sits in the corner, chilling */
31+
data object Resting : State({ setOf(Asleep) })
32+
33+
/** Zzzzzzzzz */
34+
data object Asleep : State({ setOf(Awake) })
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package app.cash.kfsm.exemplar
2+
3+
import app.cash.kfsm.Transition
4+
import app.cash.kfsm.exemplar.Hamster.Asleep
5+
import app.cash.kfsm.exemplar.Hamster.Awake
6+
import app.cash.kfsm.exemplar.Hamster.Eating
7+
import app.cash.kfsm.exemplar.Hamster.Resting
8+
import app.cash.kfsm.exemplar.Hamster.RunningOnWheel
9+
import app.cash.quiver.extensions.ErrorOr
10+
import arrow.core.NonEmptySet
11+
import arrow.core.left
12+
import arrow.core.nonEmptySetOf
13+
import arrow.core.right
14+
15+
// Create your own base transition class in order to extend your transition collection with common functionality
16+
abstract class HamsterTransition(
17+
from: NonEmptySet<Hamster.State>,
18+
to: Hamster.State
19+
) : Transition<Hamster, Hamster.State>(from, to) {
20+
constructor(from: Hamster.State, to: Hamster.State) : this(nonEmptySetOf(from), to)
21+
22+
// Demonstrates how you can add base behaviour to transitions for use in pre and post hooks.
23+
open val description: String = ""
24+
}
25+
26+
class EatBreakfast(private val food: String) : HamsterTransition(from = Awake, to = Eating) {
27+
override suspend fun effect(value: Hamster): ErrorOr<Hamster> =
28+
when (food) {
29+
"broccoli" -> {
30+
value.eat(food)
31+
value.right()
32+
}
33+
34+
"cheese" -> LactoseIntoleranceTroubles(food).left()
35+
else -> value.right()
36+
}
37+
38+
override val description = "eating $food for breakfast"
39+
}
40+
41+
object RunOnWheel : HamsterTransition(from = Eating, to = RunningOnWheel) {
42+
override suspend fun effect(value: Hamster): ErrorOr<Hamster> {
43+
// This println represents a side-effect
44+
println("$value moves to the wheel")
45+
return value.right()
46+
}
47+
48+
override val description = "running on the wheel"
49+
}
50+
51+
object GoToBed : HamsterTransition(from = nonEmptySetOf(Eating, RunningOnWheel, Resting), to = Asleep) {
52+
override suspend fun effect(value: Hamster): ErrorOr<Hamster> {
53+
value.sleep()
54+
return value.right()
55+
}
56+
57+
override val description = "going to bed"
58+
}
59+
60+
object FlakeOut : HamsterTransition(from = nonEmptySetOf(Eating, RunningOnWheel), to = Resting) {
61+
override suspend fun effect(value: Hamster): ErrorOr<Hamster> {
62+
println("$value has had enough and is sitting cute")
63+
return value.right()
64+
}
65+
66+
override val description = "tapping out"
67+
}
68+
69+
object WakeUp : HamsterTransition(from = Asleep, to = Awake) {
70+
override suspend fun effect(value: Hamster): ErrorOr<Hamster> {
71+
println("$value opens her eyes")
72+
return value.right()
73+
}
74+
75+
override val description = "waking up"
76+
}
77+
78+
data class LactoseIntoleranceTroubles(val consumed: String) : Exception("Hamster tummy troubles eating $consumed")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package app.cash.kfsm.exemplar
2+
3+
import app.cash.kfsm.StateMachine
4+
import app.cash.kfsm.Transitioner
5+
import app.cash.kfsm.exemplar.Hamster.Asleep
6+
import app.cash.kfsm.exemplar.Hamster.Awake
7+
import app.cash.kfsm.exemplar.Hamster.Eating
8+
import app.cash.kfsm.exemplar.Hamster.RunningOnWheel
9+
import app.cash.kfsm.exemplar.Hamster.State
10+
import app.cash.quiver.extensions.ErrorOr
11+
import arrow.core.Either
12+
import arrow.core.flatMap
13+
import arrow.core.right
14+
import io.kotest.assertions.arrow.core.shouldBeLeft
15+
import io.kotest.assertions.arrow.core.shouldBeRight
16+
import io.kotest.core.spec.style.StringSpec
17+
import io.kotest.matchers.collections.shouldBeEmpty
18+
import io.kotest.matchers.shouldBe
19+
20+
class PenelopePerfectDayTest : StringSpec({
21+
22+
val hamster = Hamster(name = "Penelope", state = Awake)
23+
24+
val saves = mutableListOf<Hamster>()
25+
val locks = mutableListOf<Hamster>()
26+
val unlocks = mutableListOf<Hamster>()
27+
val notifications = mutableListOf<String>()
28+
29+
// If you do not require pre and post hooks, you can simply instantiate a transitioner & provide the persistence
30+
// function as a constructor argument.
31+
// In this example, we extend the transitioner in order to define hooks that will be executed before each transition
32+
// and after each successful transition.
33+
val transitioner = object : Transitioner<HamsterTransition, Hamster, State>(
34+
// This is where you define how to save your updated value to a data store
35+
persist = { it.also(saves::add).right() }
36+
) {
37+
38+
// Any action you might wish to take prior to transitioning, such as pessimistic locking
39+
override suspend fun preHook(value: Hamster, via: HamsterTransition): ErrorOr<Unit> = Either.catch {
40+
locks.add(value)
41+
}
42+
43+
// Any action you might wish to take after transitioning successfully, such as sending events or notifications
44+
override suspend fun postHook(from: State, value: Hamster, via: HamsterTransition): ErrorOr<Unit> = Either.catch {
45+
notifications.add("${value.name} was $from, then began ${via.description} and is now ${via.to}")
46+
}
47+
}
48+
49+
beforeTest {
50+
setOf(locks, unlocks, saves, notifications).forEach { it.clear() }
51+
}
52+
53+
"a newly woken hamster eats broccoli" {
54+
val result = transitioner.transition(hamster, EatBreakfast("broccoli")).shouldBeRight()
55+
result.state shouldBe Eating
56+
locks shouldBe listOf(hamster)
57+
saves shouldBe listOf(result)
58+
notifications shouldBe listOf("Penelope was Awake, then began eating broccoli for breakfast and is now Eating")
59+
}
60+
61+
"the hamster has trouble eating cheese" {
62+
transitioner.transition(hamster, EatBreakfast("cheese")) shouldBeLeft
63+
LactoseIntoleranceTroubles("cheese")
64+
locks shouldBe listOf(hamster)
65+
saves.shouldBeEmpty()
66+
notifications.shouldBeEmpty()
67+
}
68+
69+
"a sleeping hamster can awaken yet again" {
70+
transitioner.transition(hamster, EatBreakfast("broccoli"))
71+
.flatMap { transitioner.transition(it, RunOnWheel) }
72+
.flatMap { transitioner.transition(it, GoToBed) }
73+
.flatMap { transitioner.transition(it, WakeUp) }
74+
.flatMap { transitioner.transition(it, EatBreakfast("broccoli")) }
75+
.shouldBeRight().state shouldBe Eating
76+
locks shouldBe listOf(
77+
hamster,
78+
hamster.copy(state = Eating),
79+
hamster.copy(state = RunningOnWheel),
80+
hamster.copy(state = Asleep),
81+
hamster.copy(state = Awake),
82+
)
83+
saves shouldBe listOf(
84+
hamster.copy(state = Eating),
85+
hamster.copy(state = RunningOnWheel),
86+
hamster.copy(state = Asleep),
87+
hamster.copy(state = Awake),
88+
hamster.copy(state = Eating),
89+
)
90+
notifications shouldBe listOf(
91+
"Penelope was Awake, then began eating broccoli for breakfast and is now Eating",
92+
"Penelope was Eating, then began running on the wheel and is now RunningOnWheel",
93+
"Penelope was RunningOnWheel, then began going to bed and is now Asleep",
94+
"Penelope was Asleep, then began waking up and is now Awake",
95+
"Penelope was Awake, then began eating broccoli for breakfast and is now Eating"
96+
)
97+
}
98+
99+
"a sleeping hamster cannot immediately start running on the wheel" {
100+
transitioner.transition(hamster.copy(state = Asleep), RunOnWheel).shouldBeLeft()
101+
locks.shouldBeEmpty()
102+
saves.shouldBeEmpty()
103+
notifications.shouldBeEmpty()
104+
}
105+
106+
"an eating hamster who wants to eat twice as hard will just keep eating" {
107+
val eatingHamster = hamster.copy(state = Eating)
108+
transitioner.transition(eatingHamster, EatBreakfast("broccoli"))
109+
.shouldBeRight(eatingHamster)
110+
locks.shouldBeEmpty()
111+
saves.shouldBeEmpty()
112+
notifications.shouldBeEmpty()
113+
}
114+
115+
// Add a test like this to ensure you don't have states that cannot be reached
116+
"the state machine is hunky dory" {
117+
StateMachine.verify(Awake).shouldBeRight()
118+
}
119+
120+
// Use this method to create mermaid diagrams in your markdown.
121+
// TODO(jem) - add a custom kotest matcher for ensuring the markdown is in a specific project file.
122+
"the mermaid diagram should be correct" {
123+
StateMachine.mermaid(Awake).shouldBeRight(
124+
"""stateDiagram-v2
125+
[*] --> Awake
126+
Asleep --> Awake
127+
Awake --> Eating
128+
Eating --> Asleep
129+
Eating --> Resting
130+
Eating --> RunningOnWheel
131+
Resting --> Asleep
132+
RunningOnWheel --> Asleep
133+
RunningOnWheel --> Resting
134+
""".trimIndent()
135+
)
136+
}
137+
})

0 commit comments

Comments
 (0)
Please sign in to comment.