1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
|
// Package fsm defines a finite state machine
package fsm
import "fmt"
// State represents a single node in the graph.
type State struct {
// Transitions indicates which out arrows this node has
Transitions []Transition
}
type (
// StateID represents the Identifier of the state.
StateID int8
// StateIDSlice represents the list of state identifiers.
StateIDSlice []StateID
// States is the map from state identifier to the state itself
States map[StateID]State
)
// Len is defined here such that we can sort the slice
func (v StateIDSlice) Len() int {
return len(v)
}
// Less is defined here such that we can sort the slice
func (v StateIDSlice) Less(i, j int) bool {
return v[i] < v[j]
}
// Swap is defined here such that we can sort the slice
func (v StateIDSlice) Swap(i, j int) {
v[i], v[j] = v[j], v[i]
}
// Transition indicates an arrow in the state graph.
type Transition struct {
// To represents the to-be-new state
To StateID
// Description is what type of message the arrow gets in the graph
Description string
}
// FSM represents the total graph.
type FSM struct {
// States is the map from state ID to states
States States
// Current is the current state represented by the identifier
Current StateID
// Name represents the descriptive name of this state machine
Name string
// StateCallback is the function ran when a transition occurs
// It takes the old state, the new state and the data and returns if this is handled by the client
StateCallback func(StateID, StateID, any) bool
// GetStateName gets the name of a state as a string
GetStateName func(StateID) string
// initial is the initial state that we can always go back to
initial StateID
}
// NewFSM creates a new finite state machine
func NewFSM(current StateID, states States, callback func(StateID, StateID, any) bool, nameGen func(StateID) string) FSM {
return FSM{
States: states,
Current: current,
StateCallback: callback,
GetStateName: nameGen,
initial: current,
}
}
// InState returns whether or not the state machine is in the given 'check' state.
func (fsm *FSM) InState(check StateID) bool {
return check == fsm.Current
}
// CheckTransition returns an error whether or not a transition to
// state `desired` is possible
func (fsm *FSM) CheckTransition(desired StateID) error {
// initial or begin state is fine
// 0 = deregistered
if desired == fsm.initial || desired == 0 {
return nil
}
for _, ts := range fsm.States[fsm.Current].Transitions {
if ts.To == desired {
return nil
}
}
return fmt.Errorf("fsm invalid transition attempt from '%s' to '%s'", fsm.GetStateName(fsm.Current), fsm.GetStateName(desired))
}
// GoTransitionRequired transitions the state machine to a new state with associated state data 'data'
// If this transition is not handled by the client, it returns an error.
func (fsm *FSM) GoTransitionRequired(newState StateID, data any) error {
oldState := fsm.Current
handled, err := fsm.GoTransitionWithData(newState, data)
// transition ios not possible
if err != nil {
return err
}
// transition is not handled
if !handled {
return fmt.Errorf("fsm failed transition from '%s' to '%s', is this required transition handled?", fsm.GetStateName(oldState), fsm.GetStateName(newState))
}
return nil
}
// GoTransitionWithData is a helper that transitions the state machine toward the 'newState' with associated state data 'data'
// It returns whether or not the transition is handled by the client.
func (fsm *FSM) GoTransitionWithData(newState StateID, data any) (bool, error) {
if err := fsm.CheckTransition(newState); err != nil {
return false, err
}
prev := fsm.Current
fsm.Current = newState
return fsm.StateCallback(prev, newState, data), nil
}
// GoTransition is an alias to call GoTransitionWithData but have an empty string as data.
func (fsm *FSM) GoTransition(newState StateID) (bool, error) {
// No data means the callback is never required
return fsm.GoTransitionWithData(newState, "")
}
|