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
132
133
134
135
136
137
138
139
140
|
// Package fsm defines a finite state machine
package fsm
import "fmt"
type (
// StateID represents the Identifier of the state.
StateID int8
// StateIDSlice represents the list of state identifiers.
StateIDSlice []StateID
)
// 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
}
// States is the map from state identifier to the state itself
type States map[StateID]State
// State represents a single node in the graph.
type State struct {
// Transitions indicates which out arrows this node has
Transitions []Transition
}
// 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, interface{}) bool
// Directory represents the path where the state graph is stored
Directory string
// 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
}
// Init initializes the state machine and sets it to the given current state.
func (fsm *FSM) Init(
current StateID,
states States,
callback func(StateID, StateID, interface{}) bool,
directory string,
nameGen func(StateID) string,
) {
fsm.States = states
fsm.Current = current
fsm.StateCallback = callback
fsm.Directory = directory
fsm.GetStateName = nameGen
fsm.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 interface{}) 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 interface{}) (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, "")
}
|