From b52d291785927809f60b0569bcfab6e72791d4b5 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Tue, 27 Sep 2022 15:01:17 +0200 Subject: [PATCH] [utils] Add state machine implementation --- codex/utils/statemachine.nim | 86 ++++++++++++++++++++++++++ tests/codex/testutils.nim | 1 + tests/codex/utils/teststatemachine.nim | 48 ++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 codex/utils/statemachine.nim create mode 100644 tests/codex/utils/teststatemachine.nim diff --git a/codex/utils/statemachine.nim b/codex/utils/statemachine.nim new file mode 100644 index 00000000..12898e5f --- /dev/null +++ b/codex/utils/statemachine.nim @@ -0,0 +1,86 @@ +import pkg/questionable +import ./optionalcast + +## Implementation of the the state pattern: +## https://en.wikipedia.org/wiki/State_pattern +## +## Define your own state machine and state types: +## +## type +## Light = ref object of StateMachine +## color: string +## LightState = ref object of State +## +## let light = Light(color: "yellow") +## +## Define the states: +## +## type +## On = ref object of LightState +## Off = ref object of LightState +## +## Perform actions on state entry and exit: +## +## method enter(state: On) = +## echo light.color, " light switched on" +## +## method exit(state: On) = +## echo light.color, " light no longer switched on" +## +## light.switch(On()) # prints: 'light switched on' +## light.switch(Off()) # prints: 'light no longer switched on' +## +## Allow behaviour to change based on the current state: +## +## method description*(state: LightState): string {.base.} = +## return "a light" +## +## method description*(state: On): string = +## if light =? (state.context as Light): +## return "a " & light.color & " light" +## +## method description*(state: Off): string = +## return "a dark light" +## +## proc description*(light: Light): string = +## if state =? (light.state as LightState): +## return state.description +## +## light.switch(On()) +## echo light.description # prints: 'a yellow light' +## light.switch(Off()) +## echo light.description # prints 'a dark light' + + +export questionable +export optionalcast + +type + StateMachine* = ref object of RootObj + state: ?State + State* = ref object of RootObj + context: ?StateMachine + +method enter(state: State) {.base.} = + discard + +method exit(state: State) {.base.} = + discard + +func state*(machine: StateMachine): ?State = + machine.state + +func context*(state: State): ?StateMachine = + state.context + +proc switch*(machine: StateMachine, newState: State) = + if state =? machine.state: + state.exit() + state.context = StateMachine.none + machine.state = newState.some + newState.context = machine.some + newState.enter() + +proc switch*(oldState, newState: State) = + if context =? oldState.context: + context.switch(newState) diff --git a/tests/codex/testutils.nim b/tests/codex/testutils.nim index 4ef7cee8..6e75e15a 100644 --- a/tests/codex/testutils.nim +++ b/tests/codex/testutils.nim @@ -1,3 +1,4 @@ +import ./utils/teststatemachine import ./utils/testoptionalcast {.warning[UnusedImport]: off.} diff --git a/tests/codex/utils/teststatemachine.nim b/tests/codex/utils/teststatemachine.nim new file mode 100644 index 00000000..84a573c3 --- /dev/null +++ b/tests/codex/utils/teststatemachine.nim @@ -0,0 +1,48 @@ +import std/unittest +import pkg/questionable +import codex/utils/statemachine + +type + Light = ref object of StateMachine + On = ref object of State + Off = ref object of State + +var enteredOn: bool +var exitedOn: bool + +method enter(state: On) = + enteredOn = true + +method exit(state: On) = + exitedOn = true + +suite "state machines": + + setup: + enteredOn = false + exitedOn = false + + test "calls `enter` when entering state": + Light().switch(On()) + check enteredOn + + test "calls `exit` when exiting state": + let light = Light() + light.switch(On()) + check not exitedOn + light.switch(Off()) + check exitedOn + + test "allows access to state machine from state": + let light = Light() + let on = On() + check not isSome on.context + light.switch(on) + check on.context == some StateMachine(light) + + test "removes access to state machine when state exited": + let light = Light() + let on = On() + light.switch(on) + light.switch(Off()) + check not isSome on.context