├── fsm_test.go └── fsm.go /fsm_test.go: -------------------------------------------------------------------------------- 1 | package fsm 2 | 3 | import "testing" 4 | 5 | type testTokenMachineContext struct { 6 | count int 7 | char rune 8 | entered int 9 | } 10 | 11 | func (ctx *testTokenMachineContext) StateMachineCallback(action string, args []interface{}) { 12 | switch action { 13 | case "token_inc": 14 | ctx.count++ 15 | ctx.char = args[0].(rune) 16 | case "enter": 17 | ctx.entered++ 18 | case "exit": 19 | ctx.entered = 7 20 | case "default": 21 | ctx.entered = 88 22 | } 23 | } 24 | 25 | func TestTokenMachine(t *testing.T) { 26 | var ctx testTokenMachineContext 27 | 28 | tm := NewStateMachine(&ctx, 29 | Transition{From: "locked", Event: "coin", To: "unlocked", Action: "token_inc"}, 30 | Transition{From: "locked", Event: OnEntry, Action: "enter"}, 31 | Transition{From: "locked", Event: Default, To: "locked", Action: "default"}, 32 | Transition{From: "unlocked", Event: "turn", To: "locked"}, 33 | Transition{From: "unlocked", Event: OnExit, Action: "exit"}, 34 | ) 35 | 36 | var e Error 37 | 38 | if tm.currentState.From != "locked" { 39 | t.Errorf("state machine failure") 40 | } 41 | if ctx.count != 0 { 42 | t.Errorf("state machine failure") 43 | } 44 | if ctx.char != 0 { 45 | t.Errorf("state machine failure") 46 | } 47 | 48 | e = tm.Process("coin", 'i') 49 | if e != nil { 50 | t.Errorf("state machine failure") 51 | } 52 | if tm.currentState.From != "unlocked" { 53 | t.Errorf("state machine failure") 54 | } 55 | if ctx.count != 1 { 56 | t.Errorf("state machine failure") 57 | } 58 | if ctx.char != 'i' { 59 | t.Errorf("state machine failure") 60 | } 61 | 62 | e = tm.Process("foobar", 'i') 63 | if e != nil) { 64 | t.Errorf("state machine failure") 65 | } 66 | if !(e.BadEvent() != "foobar" { 67 | t.Errorf("state machine failure") 68 | } 69 | if e.InState() != "unlocked" { 70 | t.Errorf("state machine failure") 71 | } 72 | if e.Error() != "state machine error: cannot find transition for event [foobar] when in state [unlocked]\n" { 73 | t.Errorf("state machine failure") 74 | } 75 | if tm.currentState.From != "unlocked" { 76 | t.Errorf("state machine failure") 77 | } 78 | if ctx.count != 1 { 79 | t.Errorf("state machine failure") 80 | } 81 | if ctx.char != 'i' { 82 | t.Errorf("state machine failure") 83 | } 84 | 85 | e = tm.Process("turn", 'q') 86 | if e != nil { 87 | t.Errorf("state machine failure") 88 | } 89 | if tm.currentState.From != "locked" { 90 | t.Errorf("state machine failure") 91 | } 92 | if ctx.count != 1 { 93 | t.Errorf("state machine failure") 94 | } 95 | if ctx.entered != 8 { 96 | t.Errorf("state machine failure, %d", ctx.entered) 97 | } 98 | 99 | e = tm.Process("random", 'p') 100 | if e != nil { 101 | t.Errorf("state machine failure") 102 | } 103 | if tm.currentState.From != "locked" { 104 | t.Errorf("state machine failure") 105 | } 106 | if ctx.entered != 88 { 107 | t.Errorf("state machine failure, %d", ctx.entered) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /fsm.go: -------------------------------------------------------------------------------- 1 | // Finite State Machines, in idiomatic Go1. 2 | // 3 | // Here is the basic API: 4 | // 5 | // sm := NewStateMachine(&delegate, 6 | // 7 | // Transition{ From: "locked", Event: "coin", To: "unlocked", Action: "token_inc" }, 8 | // Transition{ From: "locked", Event: OnEntry, Action: "enter" }, 9 | // Transition{ From: "locked", Event: Default, To: "locked", Action: "default" }, 10 | // 11 | // Transition{ From: "unlocked", Event: "turn", To: "locked", }, 12 | // Transition{ From: "unlocked", Event: OnExit, Action: "exit" }, 13 | // 14 | // ) 15 | // 16 | // sm.Process("coin") 17 | // sm.Process("turn", optionalArg, ...) 18 | // sm.Process("break") 19 | // 20 | // For a more complete usage, see the test file. 21 | package fsm 22 | 23 | import "fmt" 24 | 25 | const ( 26 | OnEntry = "ON_ENTRY" 27 | OnExit = "ON_EXIT" 28 | Default = "DEFAULT" 29 | ) 30 | 31 | type Transition struct { 32 | From string 33 | Event string 34 | To string 35 | Action string 36 | } 37 | 38 | // 'action' corresponds to what's in a Transition 39 | type Delegate interface { 40 | StateMachineCallback(action string, args []interface{}) 41 | } 42 | 43 | type StateMachine struct { 44 | delegate Delegate 45 | transitions []Transition 46 | currentState *Transition 47 | } 48 | 49 | // Satisfies the built-in interface 'error' 50 | type Error interface { 51 | error 52 | BadEvent() string 53 | InState() string 54 | } 55 | 56 | type smError struct { 57 | badEvent string 58 | inState string 59 | } 60 | 61 | func (e smError) Error() string { 62 | return fmt.Sprintf("state machine error: cannot find transition for event [%s] when in state [%s]\n", e.badEvent, e.inState) 63 | } 64 | 65 | func (e smError) InState() string { 66 | return e.inState 67 | } 68 | 69 | func (e smError) BadEvent() string { 70 | return e.badEvent 71 | } 72 | 73 | // Use this in conjunction with Transition literals, keeping 74 | // in mind that To may be omitted for actions, and Action may 75 | // always be omitted. See the overview above for an example. 76 | func NewStateMachine(delegate Delegate, transitions ...Transition) StateMachine { 77 | return StateMachine{delegate: delegate, transitions: transitions, currentState: &transitions[0]} 78 | } 79 | 80 | func (m *StateMachine) Process(event string, args ...interface{}) Error { 81 | trans := m.findTransMatching(m.currentState.From, event) 82 | if trans == nil { 83 | trans = m.findTransMatching(m.currentState.From, Default) 84 | } 85 | 86 | if trans == nil { 87 | return smError{event, m.currentState.From} 88 | } 89 | 90 | changing_states := trans.From != trans.To 91 | 92 | if changing_states { 93 | m.runAction(m.currentState.From, OnExit, args) 94 | } 95 | 96 | if trans.Action != "" { 97 | m.delegate.StateMachineCallback(trans.Action, args) 98 | } 99 | 100 | if changing_states { 101 | m.runAction(trans.To, OnEntry, args) 102 | } 103 | 104 | m.currentState = m.findState(trans.To) 105 | 106 | return nil 107 | } 108 | 109 | func (m *StateMachine) findTransMatching(fromState string, event string) *Transition { 110 | for _, v := range m.transitions { 111 | if v.From == fromState && v.Event == event { 112 | return &v 113 | } 114 | } 115 | return nil 116 | } 117 | 118 | func (m *StateMachine) runAction(state string, event string, args []interface{}) { 119 | if trans := m.findTransMatching(state, event); trans != nil && trans.Action != "" { 120 | m.delegate.StateMachineCallback(trans.Action, args) 121 | } 122 | } 123 | 124 | func (m *StateMachine) findState(state string) *Transition { 125 | for _, v := range m.transitions { 126 | if v.From == state { 127 | return &v 128 | } 129 | } 130 | return nil 131 | } 132 | --------------------------------------------------------------------------------