From b7a4085a49e97204338cf3098cb5f083c8928e7d Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 6 Jul 2022 22:56:37 +0200 Subject: [PATCH 01/33] Part 1, generic Events --- event.go | 12 ++- fsm.go | 68 ++++++++-------- fsm_test.go | 152 ++++++++++++++++++------------------ go.mod | 2 +- graphviz_visualizer.go | 2 +- graphviz_visualizer_test.go | 2 +- mermaid_visualizer.go | 6 +- mermaid_visualizer_test.go | 4 +- visualizer.go | 2 +- 9 files changed, 127 insertions(+), 123 deletions(-) diff --git a/event.go b/event.go index 6707198..151238a 100644 --- a/event.go +++ b/event.go @@ -14,10 +14,14 @@ package fsm +type FSMEvent interface { + string +} + // Event is the info that get passed as a reference in the callbacks. -type Event struct { +type Event[E FSMEvent] struct { // FSM is an reference to the current FSM. - FSM *FSM + FSM *FSM[E] // Event is the event name. Event string @@ -44,7 +48,7 @@ type Event struct { // Cancel can be called in before_ or leave_ to cancel the // current transition before it happens. It takes an optional error, which will // overwrite e.Err if set before. -func (e *Event) Cancel(err ...error) { +func (e *Event[E]) Cancel(err ...error) { e.canceled = true if len(err) > 0 { @@ -57,6 +61,6 @@ func (e *Event) Cancel(err ...error) { // The current state transition will be on hold in the old state until a final // call to Transition is made. This will complete the transition and possibly // call the other callbacks. -func (e *Event) Async() { +func (e *Event[E]) Async() { e.async = true } diff --git a/fsm.go b/fsm.go index f5c8cf8..60359a4 100644 --- a/fsm.go +++ b/fsm.go @@ -30,14 +30,14 @@ import ( ) // transitioner is an interface for the FSM's transition function. -type transitioner interface { - transition(*FSM) error +type transitioner[E FSMEvent] interface { + transition(*FSM[E]) error } // FSM is the state machine that holds the current state. // // It has to be created with NewFSM to function properly. -type FSM struct { +type FSM[E FSMEvent] struct { // current is the state that the FSM is currently in. current string @@ -45,13 +45,13 @@ type FSM struct { transitions map[eKey]string // callbacks maps events and targets to callback functions. - callbacks map[cKey]Callback + callbacks map[cKey]Callback[E] // transition is the internal transition functions used either directly // or when Transition is called in an asynchronous state transition. transition func() // transitionerObj calls the FSM's transition() function. - transitionerObj transitioner + transitionerObj transitioner[E] // stateMu guards access to the current state. stateMu sync.RWMutex @@ -84,13 +84,13 @@ type EventDesc struct { // Callback is a function type that callbacks should use. Event is the current // event info as the callback happens. -type Callback func(*Event) +type Callback[E FSMEvent] func(*Event[E]) // Events is a shorthand for defining the transition map in NewFSM. type Events []EventDesc // Callbacks is a shorthand for defining the callbacks in NewFSM. -type Callbacks map[string]Callback +type Callbacks[E FSMEvent] map[string]Callback[E] // NewFSM constructs a FSM from events and callbacks. // @@ -128,12 +128,12 @@ type Callbacks map[string]Callback // which version of the callback will end up in the internal map. This is due // to the pseudo random nature of Go maps. No checking for multiple keys is // currently performed. -func NewFSM(initial string, events []EventDesc, callbacks map[string]Callback) *FSM { - f := &FSM{ - transitionerObj: &transitionerStruct{}, +func NewFSM[E FSMEvent](initial string, events []EventDesc, callbacks map[string]Callback[E]) *FSM[E] { + f := &FSM[E]{ + transitionerObj: &transitionerStruct[E]{}, current: initial, transitions: make(map[eKey]string), - callbacks: make(map[cKey]Callback), + callbacks: make(map[cKey]Callback[E]), metadata: make(map[string]interface{}), } @@ -205,14 +205,14 @@ func NewFSM(initial string, events []EventDesc, callbacks map[string]Callback) * } // Current returns the current state of the FSM. -func (f *FSM) Current() string { +func (f *FSM[E]) Current() string { f.stateMu.RLock() defer f.stateMu.RUnlock() return f.current } // Is returns true if state is the current state. -func (f *FSM) Is(state string) bool { +func (f *FSM[E]) Is(state string) bool { f.stateMu.RLock() defer f.stateMu.RUnlock() return state == f.current @@ -220,14 +220,14 @@ func (f *FSM) Is(state string) bool { // SetState allows the user to move to the given state from current state. // The call does not trigger any callbacks, if defined. -func (f *FSM) SetState(state string) { +func (f *FSM[E]) SetState(state string) { f.stateMu.Lock() defer f.stateMu.Unlock() f.current = state } // Can returns true if event can occur in the current state. -func (f *FSM) Can(event string) bool { +func (f *FSM[E]) Can(event string) bool { f.stateMu.RLock() defer f.stateMu.RUnlock() _, ok := f.transitions[eKey{event, f.current}] @@ -236,7 +236,7 @@ func (f *FSM) Can(event string) bool { // AvailableTransitions returns a list of transitions available in the // current state. -func (f *FSM) AvailableTransitions() []string { +func (f *FSM[E]) AvailableTransitions() []string { f.stateMu.RLock() defer f.stateMu.RUnlock() var transitions []string @@ -250,12 +250,12 @@ func (f *FSM) AvailableTransitions() []string { // Cannot returns true if event can not occur in the current state. // It is a convenience method to help code read nicely. -func (f *FSM) Cannot(event string) bool { +func (f *FSM[E]) Cannot(event string) bool { return !f.Can(event) } // Metadata returns the value stored in metadata -func (f *FSM) Metadata(key string) (interface{}, bool) { +func (f *FSM[E]) Metadata(key string) (interface{}, bool) { f.metadataMu.RLock() defer f.metadataMu.RUnlock() dataElement, ok := f.metadata[key] @@ -263,7 +263,7 @@ func (f *FSM) Metadata(key string) (interface{}, bool) { } // SetMetadata stores the dataValue in metadata indexing it with key -func (f *FSM) SetMetadata(key string, dataValue interface{}) { +func (f *FSM[E]) SetMetadata(key string, dataValue interface{}) { f.metadataMu.Lock() defer f.metadataMu.Unlock() f.metadata[key] = dataValue @@ -286,7 +286,7 @@ func (f *FSM) SetMetadata(key string, dataValue interface{}) { // // The last error should never occur in this situation and is a sign of an // internal bug. -func (f *FSM) Event(event string, args ...interface{}) error { +func (f *FSM[E]) Event(event E, args ...interface{}) error { f.eventMu.Lock() defer f.eventMu.Unlock() @@ -294,20 +294,20 @@ func (f *FSM) Event(event string, args ...interface{}) error { defer f.stateMu.RUnlock() if f.transition != nil { - return InTransitionError{event} + return InTransitionError{string(event)} } - dst, ok := f.transitions[eKey{event, f.current}] + dst, ok := f.transitions[eKey{string(event), f.current}] if !ok { for ekey := range f.transitions { - if ekey.event == event { - return InvalidEventError{event, f.current} + if ekey.event == string(event) { + return InvalidEventError{string(event), f.current} } } - return UnknownEventError{event} + return UnknownEventError{string(event)} } - e := &Event{f, event, f.current, dst, nil, args, false, false} + e := &Event[E]{f, string(event), f.current, dst, nil, args, false, false} err := f.beforeEventCallbacks(e) if err != nil { @@ -348,26 +348,26 @@ func (f *FSM) Event(event string, args ...interface{}) error { } // Transition wraps transitioner.transition. -func (f *FSM) Transition() error { +func (f *FSM[E]) Transition() error { f.eventMu.Lock() defer f.eventMu.Unlock() return f.doTransition() } // doTransition wraps transitioner.transition. -func (f *FSM) doTransition() error { +func (f *FSM[E]) doTransition() error { return f.transitionerObj.transition(f) } // transitionerStruct is the default implementation of the transitioner // interface. Other implementations can be swapped in for testing. -type transitionerStruct struct{} +type transitionerStruct[E FSMEvent] struct{} // Transition completes an asynchronous state change. // // The callback for leave_ must previously have called Async on its // event to have initiated an asynchronous state transition. -func (t transitionerStruct) transition(f *FSM) error { +func (t transitionerStruct[E]) transition(f *FSM[E]) error { if f.transition == nil { return NotInTransitionError{} } @@ -378,7 +378,7 @@ func (t transitionerStruct) transition(f *FSM) error { // beforeEventCallbacks calls the before_ callbacks, first the named then the // general version. -func (f *FSM) beforeEventCallbacks(e *Event) error { +func (f *FSM[E]) beforeEventCallbacks(e *Event[E]) error { if fn, ok := f.callbacks[cKey{e.Event, callbackBeforeEvent}]; ok { fn(e) if e.canceled { @@ -396,7 +396,7 @@ func (f *FSM) beforeEventCallbacks(e *Event) error { // leaveStateCallbacks calls the leave_ callbacks, first the named then the // general version. -func (f *FSM) leaveStateCallbacks(e *Event) error { +func (f *FSM[E]) leaveStateCallbacks(e *Event[E]) error { if fn, ok := f.callbacks[cKey{f.current, callbackLeaveState}]; ok { fn(e) if e.canceled { @@ -418,7 +418,7 @@ func (f *FSM) leaveStateCallbacks(e *Event) error { // enterStateCallbacks calls the enter_ callbacks, first the named then the // general version. -func (f *FSM) enterStateCallbacks(e *Event) { +func (f *FSM[E]) enterStateCallbacks(e *Event[E]) { if fn, ok := f.callbacks[cKey{f.current, callbackEnterState}]; ok { fn(e) } @@ -429,7 +429,7 @@ func (f *FSM) enterStateCallbacks(e *Event) { // afterEventCallbacks calls the after_ callbacks, first the named then the // general version. -func (f *FSM) afterEventCallbacks(e *Event) { +func (f *FSM[E]) afterEventCallbacks(e *Event[E]) { if fn, ok := f.callbacks[cKey{e.Event, callbackAfterEvent}]; ok { fn(e) } diff --git a/fsm_test.go b/fsm_test.go index 431fd65..7140598 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -22,10 +22,10 @@ import ( "time" ) -type fakeTransitionerObj struct { +type fakeTransitionerObj[E FSMEvent] struct { } -func (t fakeTransitionerObj) transition(f *FSM) error { +func (t fakeTransitionerObj[E]) transition(f *FSM[E]) error { return &InternalError{} } @@ -35,7 +35,7 @@ func TestSameState(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "start"}, }, - Callbacks{}, + Callbacks[string]{}, ) _ = fsm.Event("run") if fsm.Current() != "start" { @@ -49,7 +49,7 @@ func TestSetState(t *testing.T) { Events{ {Name: "walk", Src: []string{"start"}, Dst: "walking"}, }, - Callbacks{}, + Callbacks[string]{}, ) fsm.SetState("start") if fsm.Current() != "start" { @@ -67,9 +67,9 @@ func TestBadTransition(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "running"}, }, - Callbacks{}, + Callbacks[string]{}, ) - fsm.transitionerObj = new(fakeTransitionerObj) + fsm.transitionerObj = new(fakeTransitionerObj[string]) err := fsm.Event("run") if err == nil { t.Error("bad transition should give an error") @@ -83,7 +83,7 @@ func TestInappropriateEvent(t *testing.T) { {Name: "open", Src: []string{"closed"}, Dst: "open"}, {Name: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks{}, + Callbacks[string]{}, ) err := fsm.Event("close") if e, ok := err.(InvalidEventError); !ok && e.Event != "close" && e.State != "closed" { @@ -98,7 +98,7 @@ func TestInvalidEvent(t *testing.T) { {Name: "open", Src: []string{"closed"}, Dst: "open"}, {Name: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks{}, + Callbacks[string]{}, ) err := fsm.Event("lock") if e, ok := err.(UnknownEventError); !ok && e.Event != "close" { @@ -114,7 +114,7 @@ func TestMultipleSources(t *testing.T) { {Name: "second", Src: []string{"two"}, Dst: "three"}, {Name: "reset", Src: []string{"one", "two", "three"}, Dst: "one"}, }, - Callbacks{}, + Callbacks[string]{}, ) err := fsm.Event("first") @@ -161,7 +161,7 @@ func TestMultipleEvents(t *testing.T) { {Name: "reset", Src: []string{"two"}, Dst: "reset_two"}, {Name: "reset", Src: []string{"reset_one", "reset_two"}, Dst: "start"}, }, - Callbacks{}, + Callbacks[string]{}, ) err := fsm.Event("first") @@ -214,17 +214,17 @@ func TestGenericCallbacks(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "before_event": func(e *Event) { + Callbacks[string]{ + "before_event": func(e *Event[string]) { beforeEvent = true }, - "leave_state": func(e *Event) { + "leave_state": func(e *Event[string]) { leaveState = true }, - "enter_state": func(e *Event) { + "enter_state": func(e *Event[string]) { enterState = true }, - "after_event": func(e *Event) { + "after_event": func(e *Event[string]) { afterEvent = true }, }, @@ -250,17 +250,17 @@ func TestSpecificCallbacks(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "before_run": func(e *Event) { + Callbacks[string]{ + "before_run": func(e *Event[string]) { beforeEvent = true }, - "leave_start": func(e *Event) { + "leave_start": func(e *Event[string]) { leaveState = true }, - "enter_end": func(e *Event) { + "enter_end": func(e *Event[string]) { enterState = true }, - "after_run": func(e *Event) { + "after_run": func(e *Event[string]) { afterEvent = true }, }, @@ -284,11 +284,11 @@ func TestSpecificCallbacksShortform(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "end": func(e *Event) { + Callbacks[string]{ + "end": func(e *Event[string]) { enterState = true }, - "run": func(e *Event) { + "run": func(e *Event[string]) { afterEvent = true }, }, @@ -311,8 +311,8 @@ func TestBeforeEventWithoutTransition(t *testing.T) { Events{ {Name: "dontrun", Src: []string{"start"}, Dst: "start"}, }, - Callbacks{ - "before_event": func(e *Event) { + Callbacks[string]{ + "before_event": func(e *Event[string]) { beforeEvent = true }, }, @@ -337,8 +337,8 @@ func TestCancelBeforeGenericEvent(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "before_event": func(e *Event) { + Callbacks[string]{ + "before_event": func(e *Event[string]) { e.Cancel() }, }, @@ -355,8 +355,8 @@ func TestCancelBeforeSpecificEvent(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "before_run": func(e *Event) { + Callbacks[string]{ + "before_run": func(e *Event[string]) { e.Cancel() }, }, @@ -373,8 +373,8 @@ func TestCancelLeaveGenericState(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "leave_state": func(e *Event) { + Callbacks[string]{ + "leave_state": func(e *Event[string]) { e.Cancel() }, }, @@ -391,8 +391,8 @@ func TestCancelLeaveSpecificState(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "leave_start": func(e *Event) { + Callbacks[string]{ + "leave_start": func(e *Event[string]) { e.Cancel() }, }, @@ -409,8 +409,8 @@ func TestCancelWithError(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "before_event": func(e *Event) { + Callbacks[string]{ + "before_event": func(e *Event[string]) { e.Cancel(fmt.Errorf("error")) }, }, @@ -435,8 +435,8 @@ func TestAsyncTransitionGenericState(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "leave_state": func(e *Event) { + Callbacks[string]{ + "leave_state": func(e *Event[string]) { e.Async() }, }, @@ -460,8 +460,8 @@ func TestAsyncTransitionSpecificState(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "leave_start": func(e *Event) { + Callbacks[string]{ + "leave_start": func(e *Event[string]) { e.Async() }, }, @@ -486,8 +486,8 @@ func TestAsyncTransitionInProgress(t *testing.T) { {Name: "run", Src: []string{"start"}, Dst: "end"}, {Name: "reset", Src: []string{"end"}, Dst: "start"}, }, - Callbacks{ - "leave_start": func(e *Event) { + Callbacks[string]{ + "leave_start": func(e *Event[string]) { e.Async() }, }, @@ -517,7 +517,7 @@ func TestAsyncTransitionNotInProgress(t *testing.T) { {Name: "run", Src: []string{"start"}, Dst: "end"}, {Name: "reset", Src: []string{"end"}, Dst: "start"}, }, - Callbacks{}, + Callbacks[string]{}, ) err := fsm.Transition() if _, ok := err.(NotInTransitionError); !ok { @@ -531,8 +531,8 @@ func TestCallbackNoError(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "run": func(e *Event) { + Callbacks[string]{ + "run": func(e *Event[string]) { }, }, ) @@ -548,8 +548,8 @@ func TestCallbackError(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "run": func(e *Event) { + Callbacks[string]{ + "run": func(e *Event[string]) { e.Err = fmt.Errorf("error") }, }, @@ -566,8 +566,8 @@ func TestCallbackArgs(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "run": func(e *Event) { + Callbacks[string]{ + "run": func(e *Event[string]) { if len(e.Args) != 1 { t.Error("too few arguments") } @@ -600,8 +600,8 @@ func TestCallbackPanic(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "run": func(e *Event) { + Callbacks[string]{ + "run": func(e *Event[string]) { panic(panicMsg) }, }, @@ -613,14 +613,14 @@ func TestCallbackPanic(t *testing.T) { } func TestNoDeadLock(t *testing.T) { - var fsm *FSM + var fsm *FSM[string] fsm = NewFSM( "start", Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "run": func(e *Event) { + Callbacks[string]{ + "run": func(e *Event[string]) { fsm.Current() // Should not result in a panic / deadlock }, }, @@ -637,8 +637,8 @@ func TestThreadSafetyRaceCondition(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "run": func(e *Event) { + Callbacks[string]{ + "run": func(e *Event[string]) { }, }, ) @@ -656,7 +656,7 @@ func TestThreadSafetyRaceCondition(t *testing.T) { } func TestDoubleTransition(t *testing.T) { - var fsm *FSM + var fsm *FSM[string] var wg sync.WaitGroup wg.Add(2) fsm = NewFSM( @@ -664,8 +664,8 @@ func TestDoubleTransition(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks{ - "before_run": func(e *Event) { + Callbacks[string]{ + "before_run": func(e *Event[string]) { wg.Done() // Imagine a concurrent event coming in of the same type while // the data access mutex is unlocked because the current transition @@ -700,7 +700,7 @@ func TestNoTransition(t *testing.T) { Events{ {Name: "run", Src: []string{"start"}, Dst: "start"}, }, - Callbacks{}, + Callbacks[string]{}, ) err := fsm.Event("run") if _, ok := err.(NoTransitionError); !ok { @@ -718,29 +718,29 @@ func ExampleNewFSM() { {Name: "calm", Src: []string{"red"}, Dst: "yellow"}, {Name: "clear", Src: []string{"yellow"}, Dst: "green"}, }, - Callbacks{ - "before_warn": func(e *Event) { + Callbacks[string]{ + "before_warn": func(e *Event[string]) { fmt.Println("before_warn") }, - "before_event": func(e *Event) { + "before_event": func(e *Event[string]) { fmt.Println("before_event") }, - "leave_green": func(e *Event) { + "leave_green": func(e *Event[string]) { fmt.Println("leave_green") }, - "leave_state": func(e *Event) { + "leave_state": func(e *Event[string]) { fmt.Println("leave_state") }, - "enter_yellow": func(e *Event) { + "enter_yellow": func(e *Event[string]) { fmt.Println("enter_yellow") }, - "enter_state": func(e *Event) { + "enter_state": func(e *Event[string]) { fmt.Println("enter_state") }, - "after_warn": func(e *Event) { + "after_warn": func(e *Event[string]) { fmt.Println("after_warn") }, - "after_event": func(e *Event) { + "after_event": func(e *Event[string]) { fmt.Println("after_event") }, }, @@ -771,7 +771,7 @@ func ExampleFSM_Current() { {Name: "open", Src: []string{"closed"}, Dst: "open"}, {Name: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks{}, + Callbacks[string]{}, ) fmt.Println(fsm.Current()) // Output: closed @@ -784,7 +784,7 @@ func ExampleFSM_Is() { {Name: "open", Src: []string{"closed"}, Dst: "open"}, {Name: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks{}, + Callbacks[string]{}, ) fmt.Println(fsm.Is("closed")) fmt.Println(fsm.Is("open")) @@ -800,7 +800,7 @@ func ExampleFSM_Can() { {Name: "open", Src: []string{"closed"}, Dst: "open"}, {Name: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks{}, + Callbacks[string]{}, ) fmt.Println(fsm.Can("open")) fmt.Println(fsm.Can("close")) @@ -817,7 +817,7 @@ func ExampleFSM_AvailableTransitions() { {Name: "close", Src: []string{"open"}, Dst: "closed"}, {Name: "kick", Src: []string{"closed"}, Dst: "broken"}, }, - Callbacks{}, + Callbacks[string]{}, ) // sort the results ordering is consistent for the output checker transitions := fsm.AvailableTransitions() @@ -834,7 +834,7 @@ func ExampleFSM_Cannot() { {Name: "open", Src: []string{"closed"}, Dst: "open"}, {Name: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks{}, + Callbacks[string]{}, ) fmt.Println(fsm.Cannot("open")) fmt.Println(fsm.Cannot("close")) @@ -850,7 +850,7 @@ func ExampleFSM_Event() { {Name: "open", Src: []string{"closed"}, Dst: "open"}, {Name: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks{}, + Callbacks[string]{}, ) fmt.Println(fsm.Current()) err := fsm.Event("open") @@ -876,8 +876,8 @@ func ExampleFSM_Transition() { {Name: "open", Src: []string{"closed"}, Dst: "open"}, {Name: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks{ - "leave_closed": func(e *Event) { + Callbacks[string]{ + "leave_closed": func(e *Event[string]) { e.Async() }, }, diff --git a/go.mod b/go.mod index 1af2b1c..e3773db 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/looplab/fsm -go 1.16 +go 1.18 diff --git a/graphviz_visualizer.go b/graphviz_visualizer.go index 5a5b641..d6bc20e 100644 --- a/graphviz_visualizer.go +++ b/graphviz_visualizer.go @@ -6,7 +6,7 @@ import ( ) // Visualize outputs a visualization of a FSM in Graphviz format. -func Visualize(fsm *FSM) string { +func Visualize[E FSMEvent](fsm *FSM[E]) string { var buf bytes.Buffer // we sort the key alphabetically to have a reproducible graph output diff --git a/graphviz_visualizer_test.go b/graphviz_visualizer_test.go index b28c476..e8d004a 100644 --- a/graphviz_visualizer_test.go +++ b/graphviz_visualizer_test.go @@ -14,7 +14,7 @@ func TestGraphvizOutput(t *testing.T) { {Name: "close", Src: []string{"open"}, Dst: "closed"}, {Name: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, }, - Callbacks{}, + Callbacks[string]{}, ) got := Visualize(fsmUnderTest) diff --git a/mermaid_visualizer.go b/mermaid_visualizer.go index d9b089e..d62985a 100644 --- a/mermaid_visualizer.go +++ b/mermaid_visualizer.go @@ -18,7 +18,7 @@ const ( ) // VisualizeForMermaidWithGraphType outputs a visualization of a FSM in Mermaid format as specified by the graphType. -func VisualizeForMermaidWithGraphType(fsm *FSM, graphType MermaidDiagramType) (string, error) { +func VisualizeForMermaidWithGraphType[E FSMEvent](fsm *FSM[E], graphType MermaidDiagramType) (string, error) { switch graphType { case FlowChart: return visualizeForMermaidAsFlowChart(fsm), nil @@ -29,7 +29,7 @@ func VisualizeForMermaidWithGraphType(fsm *FSM, graphType MermaidDiagramType) (s } } -func visualizeForMermaidAsStateDiagram(fsm *FSM) string { +func visualizeForMermaidAsStateDiagram[E FSMEvent](fsm *FSM[E]) string { var buf bytes.Buffer sortedTransitionKeys := getSortedTransitionKeys(fsm.transitions) @@ -47,7 +47,7 @@ func visualizeForMermaidAsStateDiagram(fsm *FSM) string { } // visualizeForMermaidAsFlowChart outputs a visualization of a FSM in Mermaid format (including highlighting of current state). -func visualizeForMermaidAsFlowChart(fsm *FSM) string { +func visualizeForMermaidAsFlowChart[E FSMEvent](fsm *FSM[E]) string { var buf bytes.Buffer sortedTransitionKeys := getSortedTransitionKeys(fsm.transitions) diff --git a/mermaid_visualizer_test.go b/mermaid_visualizer_test.go index f922ba5..d391b22 100644 --- a/mermaid_visualizer_test.go +++ b/mermaid_visualizer_test.go @@ -14,7 +14,7 @@ func TestMermaidOutput(t *testing.T) { {Name: "close", Src: []string{"open"}, Dst: "closed"}, {Name: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, }, - Callbacks{}, + Callbacks[string]{}, ) got, err := VisualizeForMermaidWithGraphType(fsmUnderTest, StateDiagram) @@ -47,7 +47,7 @@ func TestMermaidFlowChartOutput(t *testing.T) { {Name: "close", Src: []string{"open"}, Dst: "closed"}, {Name: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, }, - Callbacks{}, + Callbacks[string]{}, ) got, err := VisualizeForMermaidWithGraphType(fsmUnderTest, FlowChart) diff --git a/visualizer.go b/visualizer.go index 04cc872..a16d4fe 100644 --- a/visualizer.go +++ b/visualizer.go @@ -21,7 +21,7 @@ const ( // VisualizeWithType outputs a visualization of a FSM in the desired format. // If the type is not given it defaults to GRAPHVIZ -func VisualizeWithType(fsm *FSM, visualizeType VisualizeType) (string, error) { +func VisualizeWithType[E FSMEvent](fsm *FSM[E], visualizeType VisualizeType) (string, error) { switch visualizeType { case GRAPHVIZ: return Visualize(fsm), nil From 752ee384caecabdc8268428f3483eae3a25ff317 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 09:21:44 +0200 Subject: [PATCH 02/33] Refactor a bit --- Makefile | 2 +- event.go | 16 +- fsm.go | 66 ++++---- fsm_test.go | 293 ++++++++++++++++++++---------------- graphviz_visualizer.go | 4 +- graphviz_visualizer_test.go | 8 +- mermaid_visualizer.go | 8 +- mermaid_visualizer_test.go | 20 +-- visualizer.go | 8 +- 9 files changed, 229 insertions(+), 196 deletions(-) diff --git a/Makefile b/Makefile index 70e816f..821a539 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ default: services test .PHONY: test test: - go test ./... + go test -v -race ./... .PHONY: lint lint: diff --git a/event.go b/event.go index 151238a..052373b 100644 --- a/event.go +++ b/event.go @@ -14,14 +14,14 @@ package fsm -type FSMEvent interface { - string +type Event interface { + ~string } -// Event is the info that get passed as a reference in the callbacks. -type Event[E FSMEvent] struct { - // FSM is an reference to the current FSM. - FSM *FSM[E] +// CallbackReference is the info that get passed as a reference in the callbacks. +type CallbackReference[E Event] struct { + // fsm is an reference to the current fsm. + fsm *FSM[E] // Event is the event name. Event string @@ -48,7 +48,7 @@ type Event[E FSMEvent] struct { // Cancel can be called in before_ or leave_ to cancel the // current transition before it happens. It takes an optional error, which will // overwrite e.Err if set before. -func (e *Event[E]) Cancel(err ...error) { +func (e *CallbackReference[E]) Cancel(err ...error) { e.canceled = true if len(err) > 0 { @@ -61,6 +61,6 @@ func (e *Event[E]) Cancel(err ...error) { // The current state transition will be on hold in the old state until a final // call to Transition is made. This will complete the transition and possibly // call the other callbacks. -func (e *Event[E]) Async() { +func (e *CallbackReference[E]) Async() { e.async = true } diff --git a/fsm.go b/fsm.go index 60359a4..ced2756 100644 --- a/fsm.go +++ b/fsm.go @@ -30,19 +30,19 @@ import ( ) // transitioner is an interface for the FSM's transition function. -type transitioner[E FSMEvent] interface { +type transitioner[E Event] interface { transition(*FSM[E]) error } // FSM is the state machine that holds the current state. // // It has to be created with NewFSM to function properly. -type FSM[E FSMEvent] struct { +type FSM[E Event] struct { // current is the state that the FSM is currently in. current string // transitions maps events and source states to destination states. - transitions map[eKey]string + transitions map[eKey[E]]string // callbacks maps events and targets to callback functions. callbacks map[cKey]Callback[E] @@ -64,14 +64,14 @@ type FSM[E FSMEvent] struct { metadataMu sync.RWMutex } -// EventDesc represents an event when initializing the FSM. +// StateMachineEntry represents an event when initializing the FSM. // // The event can have one or more source states that is valid for performing // the transition. If the FSM is in one of the source states it will end up in // the specified destination state, calling all defined callbacks as it goes. -type EventDesc struct { - // Name is the event name used when calling for a transition. - Name string +type StateMachineEntry[E Event] struct { + // Event is the event name used when calling for a transition. + Event E // Src is a slice of source states that the FSM must be in to perform a // state transition. @@ -84,13 +84,13 @@ type EventDesc struct { // Callback is a function type that callbacks should use. Event is the current // event info as the callback happens. -type Callback[E FSMEvent] func(*Event[E]) +type Callback[E Event] func(*CallbackReference[E]) -// Events is a shorthand for defining the transition map in NewFSM. -type Events []EventDesc +// StateMachine is a shorthand for defining the transition map in NewFSM. +type StateMachine[E Event] []StateMachineEntry[E] // Callbacks is a shorthand for defining the callbacks in NewFSM. -type Callbacks[E FSMEvent] map[string]Callback[E] +type Callbacks[E Event] map[string]Callback[E] // NewFSM constructs a FSM from events and callbacks. // @@ -128,25 +128,25 @@ type Callbacks[E FSMEvent] map[string]Callback[E] // which version of the callback will end up in the internal map. This is due // to the pseudo random nature of Go maps. No checking for multiple keys is // currently performed. -func NewFSM[E FSMEvent](initial string, events []EventDesc, callbacks map[string]Callback[E]) *FSM[E] { +func NewFSM[E Event](initial string, events []StateMachineEntry[E], callbacks map[string]Callback[E]) *FSM[E] { f := &FSM[E]{ transitionerObj: &transitionerStruct[E]{}, current: initial, - transitions: make(map[eKey]string), + transitions: make(map[eKey[E]]string), callbacks: make(map[cKey]Callback[E]), metadata: make(map[string]interface{}), } // Build transition map and store sets of all events and states. - allEvents := make(map[string]bool) + allEvents := make(map[E]bool) allStates := make(map[string]bool) for _, e := range events { for _, src := range e.Src { - f.transitions[eKey{e.Name, src}] = e.Dst + f.transitions[eKey[E]{e.Event, src}] = e.Dst allStates[src] = true allStates[e.Dst] = true } - allEvents[e.Name] = true + allEvents[e.Event] = true } // Map all callbacks to events/states. @@ -160,7 +160,7 @@ func NewFSM[E FSMEvent](initial string, events []EventDesc, callbacks map[string if target == "event" { target = "" callbackType = callbackBeforeEvent - } else if _, ok := allEvents[target]; ok { + } else if _, ok := allEvents[E(target)]; ok { // FIXME callbackType = callbackBeforeEvent } case strings.HasPrefix(name, "leave_"): @@ -184,14 +184,14 @@ func NewFSM[E FSMEvent](initial string, events []EventDesc, callbacks map[string if target == "event" { target = "" callbackType = callbackAfterEvent - } else if _, ok := allEvents[target]; ok { + } else if _, ok := allEvents[E(target)]; ok { // FIXME callbackType = callbackAfterEvent } default: target = name if _, ok := allStates[target]; ok { callbackType = callbackEnterState - } else if _, ok := allEvents[target]; ok { + } else if _, ok := allEvents[E(target)]; ok { // FIXME callbackType = callbackAfterEvent } } @@ -227,10 +227,10 @@ func (f *FSM[E]) SetState(state string) { } // Can returns true if event can occur in the current state. -func (f *FSM[E]) Can(event string) bool { +func (f *FSM[E]) Can(event E) bool { f.stateMu.RLock() defer f.stateMu.RUnlock() - _, ok := f.transitions[eKey{event, f.current}] + _, ok := f.transitions[eKey[E]{event, f.current}] return ok && (f.transition == nil) } @@ -242,7 +242,7 @@ func (f *FSM[E]) AvailableTransitions() []string { var transitions []string for key := range f.transitions { if key.src == f.current { - transitions = append(transitions, key.event) + transitions = append(transitions, string(key.event)) } } return transitions @@ -250,7 +250,7 @@ func (f *FSM[E]) AvailableTransitions() []string { // Cannot returns true if event can not occur in the current state. // It is a convenience method to help code read nicely. -func (f *FSM[E]) Cannot(event string) bool { +func (f *FSM[E]) Cannot(event E) bool { return !f.Can(event) } @@ -297,17 +297,17 @@ func (f *FSM[E]) Event(event E, args ...interface{}) error { return InTransitionError{string(event)} } - dst, ok := f.transitions[eKey{string(event), f.current}] + dst, ok := f.transitions[eKey[E]{event, f.current}] if !ok { for ekey := range f.transitions { - if ekey.event == string(event) { + if ekey.event == event { return InvalidEventError{string(event), f.current} } } return UnknownEventError{string(event)} } - e := &Event[E]{f, string(event), f.current, dst, nil, args, false, false} + e := &CallbackReference[E]{f, string(event), f.current, dst, nil, args, false, false} err := f.beforeEventCallbacks(e) if err != nil { @@ -361,7 +361,7 @@ func (f *FSM[E]) doTransition() error { // transitionerStruct is the default implementation of the transitioner // interface. Other implementations can be swapped in for testing. -type transitionerStruct[E FSMEvent] struct{} +type transitionerStruct[E Event] struct{} // Transition completes an asynchronous state change. // @@ -378,7 +378,7 @@ func (t transitionerStruct[E]) transition(f *FSM[E]) error { // beforeEventCallbacks calls the before_ callbacks, first the named then the // general version. -func (f *FSM[E]) beforeEventCallbacks(e *Event[E]) error { +func (f *FSM[E]) beforeEventCallbacks(e *CallbackReference[E]) error { if fn, ok := f.callbacks[cKey{e.Event, callbackBeforeEvent}]; ok { fn(e) if e.canceled { @@ -396,7 +396,7 @@ func (f *FSM[E]) beforeEventCallbacks(e *Event[E]) error { // leaveStateCallbacks calls the leave_ callbacks, first the named then the // general version. -func (f *FSM[E]) leaveStateCallbacks(e *Event[E]) error { +func (f *FSM[E]) leaveStateCallbacks(e *CallbackReference[E]) error { if fn, ok := f.callbacks[cKey{f.current, callbackLeaveState}]; ok { fn(e) if e.canceled { @@ -418,7 +418,7 @@ func (f *FSM[E]) leaveStateCallbacks(e *Event[E]) error { // enterStateCallbacks calls the enter_ callbacks, first the named then the // general version. -func (f *FSM[E]) enterStateCallbacks(e *Event[E]) { +func (f *FSM[E]) enterStateCallbacks(e *CallbackReference[E]) { if fn, ok := f.callbacks[cKey{f.current, callbackEnterState}]; ok { fn(e) } @@ -429,7 +429,7 @@ func (f *FSM[E]) enterStateCallbacks(e *Event[E]) { // afterEventCallbacks calls the after_ callbacks, first the named then the // general version. -func (f *FSM[E]) afterEventCallbacks(e *Event[E]) { +func (f *FSM[E]) afterEventCallbacks(e *CallbackReference[E]) { if fn, ok := f.callbacks[cKey{e.Event, callbackAfterEvent}]; ok { fn(e) } @@ -458,9 +458,9 @@ type cKey struct { } // eKey is a struct key used for storing the transition map. -type eKey struct { +type eKey[E Event] struct { // event is the name of the event that the keys refers to. - event string + event E // src is the source from where the event can transition. src string diff --git a/fsm_test.go b/fsm_test.go index 7140598..64e3939 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -22,7 +22,7 @@ import ( "time" ) -type fakeTransitionerObj[E FSMEvent] struct { +type fakeTransitionerObj[E Event] struct { } func (t fakeTransitionerObj[E]) transition(f *FSM[E]) error { @@ -32,8 +32,8 @@ func (t fakeTransitionerObj[E]) transition(f *FSM[E]) error { func TestSameState(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "start"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string]{}, ) @@ -46,8 +46,8 @@ func TestSameState(t *testing.T) { func TestSetState(t *testing.T) { fsm := NewFSM( "walking", - Events{ - {Name: "walk", Src: []string{"start"}, Dst: "walking"}, + StateMachine[string]{ + {Event: "walk", Src: []string{"start"}, Dst: "walking"}, }, Callbacks[string]{}, ) @@ -64,8 +64,8 @@ func TestSetState(t *testing.T) { func TestBadTransition(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "running"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "running"}, }, Callbacks[string]{}, ) @@ -79,9 +79,9 @@ func TestBadTransition(t *testing.T) { func TestInappropriateEvent(t *testing.T) { fsm := NewFSM( "closed", - Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, + StateMachine[string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, Callbacks[string]{}, ) @@ -94,9 +94,9 @@ func TestInappropriateEvent(t *testing.T) { func TestInvalidEvent(t *testing.T) { fsm := NewFSM( "closed", - Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, + StateMachine[string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, Callbacks[string]{}, ) @@ -109,10 +109,10 @@ func TestInvalidEvent(t *testing.T) { func TestMultipleSources(t *testing.T) { fsm := NewFSM( "one", - Events{ - {Name: "first", Src: []string{"one"}, Dst: "two"}, - {Name: "second", Src: []string{"two"}, Dst: "three"}, - {Name: "reset", Src: []string{"one", "two", "three"}, Dst: "one"}, + StateMachine[string]{ + {Event: "first", Src: []string{"one"}, Dst: "two"}, + {Event: "second", Src: []string{"two"}, Dst: "three"}, + {Event: "reset", Src: []string{"one", "two", "three"}, Dst: "one"}, }, Callbacks[string]{}, ) @@ -154,12 +154,12 @@ func TestMultipleSources(t *testing.T) { func TestMultipleEvents(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "first", Src: []string{"start"}, Dst: "one"}, - {Name: "second", Src: []string{"start"}, Dst: "two"}, - {Name: "reset", Src: []string{"one"}, Dst: "reset_one"}, - {Name: "reset", Src: []string{"two"}, Dst: "reset_two"}, - {Name: "reset", Src: []string{"reset_one", "reset_two"}, Dst: "start"}, + StateMachine[string]{ + {Event: "first", Src: []string{"start"}, Dst: "one"}, + {Event: "second", Src: []string{"start"}, Dst: "two"}, + {Event: "reset", Src: []string{"one"}, Dst: "reset_one"}, + {Event: "reset", Src: []string{"two"}, Dst: "reset_two"}, + {Event: "reset", Src: []string{"reset_one", "reset_two"}, Dst: "start"}, }, Callbacks[string]{}, ) @@ -211,20 +211,20 @@ func TestGenericCallbacks(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "before_event": func(e *Event[string]) { + "before_event": func(e *CallbackReference[string]) { beforeEvent = true }, - "leave_state": func(e *Event[string]) { + "leave_state": func(e *CallbackReference[string]) { leaveState = true }, - "enter_state": func(e *Event[string]) { + "enter_state": func(e *CallbackReference[string]) { enterState = true }, - "after_event": func(e *Event[string]) { + "after_event": func(e *CallbackReference[string]) { afterEvent = true }, }, @@ -247,20 +247,20 @@ func TestSpecificCallbacks(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "before_run": func(e *Event[string]) { + "before_run": func(e *CallbackReference[string]) { beforeEvent = true }, - "leave_start": func(e *Event[string]) { + "leave_start": func(e *CallbackReference[string]) { leaveState = true }, - "enter_end": func(e *Event[string]) { + "enter_end": func(e *CallbackReference[string]) { enterState = true }, - "after_run": func(e *Event[string]) { + "after_run": func(e *CallbackReference[string]) { afterEvent = true }, }, @@ -281,14 +281,14 @@ func TestSpecificCallbacksShortform(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "end": func(e *Event[string]) { + "end": func(e *CallbackReference[string]) { enterState = true }, - "run": func(e *Event[string]) { + "run": func(e *CallbackReference[string]) { afterEvent = true }, }, @@ -308,11 +308,11 @@ func TestBeforeEventWithoutTransition(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "dontrun", Src: []string{"start"}, Dst: "start"}, + StateMachine[string]{ + {Event: "dontrun", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string]{ - "before_event": func(e *Event[string]) { + "before_event": func(e *CallbackReference[string]) { beforeEvent = true }, }, @@ -334,11 +334,11 @@ func TestBeforeEventWithoutTransition(t *testing.T) { func TestCancelBeforeGenericEvent(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "before_event": func(e *Event[string]) { + "before_event": func(e *CallbackReference[string]) { e.Cancel() }, }, @@ -352,11 +352,11 @@ func TestCancelBeforeGenericEvent(t *testing.T) { func TestCancelBeforeSpecificEvent(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "before_run": func(e *Event[string]) { + "before_run": func(e *CallbackReference[string]) { e.Cancel() }, }, @@ -370,11 +370,11 @@ func TestCancelBeforeSpecificEvent(t *testing.T) { func TestCancelLeaveGenericState(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "leave_state": func(e *Event[string]) { + "leave_state": func(e *CallbackReference[string]) { e.Cancel() }, }, @@ -388,11 +388,11 @@ func TestCancelLeaveGenericState(t *testing.T) { func TestCancelLeaveSpecificState(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "leave_start": func(e *Event[string]) { + "leave_start": func(e *CallbackReference[string]) { e.Cancel() }, }, @@ -406,11 +406,11 @@ func TestCancelLeaveSpecificState(t *testing.T) { func TestCancelWithError(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "before_event": func(e *Event[string]) { + "before_event": func(e *CallbackReference[string]) { e.Cancel(fmt.Errorf("error")) }, }, @@ -432,11 +432,11 @@ func TestCancelWithError(t *testing.T) { func TestAsyncTransitionGenericState(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "leave_state": func(e *Event[string]) { + "leave_state": func(e *CallbackReference[string]) { e.Async() }, }, @@ -457,11 +457,11 @@ func TestAsyncTransitionGenericState(t *testing.T) { func TestAsyncTransitionSpecificState(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "leave_start": func(e *Event[string]) { + "leave_start": func(e *CallbackReference[string]) { e.Async() }, }, @@ -482,12 +482,12 @@ func TestAsyncTransitionSpecificState(t *testing.T) { func TestAsyncTransitionInProgress(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, - {Name: "reset", Src: []string{"end"}, Dst: "start"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, + {Event: "reset", Src: []string{"end"}, Dst: "start"}, }, Callbacks[string]{ - "leave_start": func(e *Event[string]) { + "leave_start": func(e *CallbackReference[string]) { e.Async() }, }, @@ -513,9 +513,9 @@ func TestAsyncTransitionInProgress(t *testing.T) { func TestAsyncTransitionNotInProgress(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, - {Name: "reset", Src: []string{"end"}, Dst: "start"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, + {Event: "reset", Src: []string{"end"}, Dst: "start"}, }, Callbacks[string]{}, ) @@ -528,11 +528,11 @@ func TestAsyncTransitionNotInProgress(t *testing.T) { func TestCallbackNoError(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "run": func(e *Event[string]) { + "run": func(e *CallbackReference[string]) { }, }, ) @@ -545,11 +545,11 @@ func TestCallbackNoError(t *testing.T) { func TestCallbackError(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "run": func(e *Event[string]) { + "run": func(e *CallbackReference[string]) { e.Err = fmt.Errorf("error") }, }, @@ -563,11 +563,11 @@ func TestCallbackError(t *testing.T) { func TestCallbackArgs(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "run": func(e *Event[string]) { + "run": func(e *CallbackReference[string]) { if len(e.Args) != 1 { t.Error("too few arguments") } @@ -597,11 +597,11 @@ func TestCallbackPanic(t *testing.T) { }() fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "run": func(e *Event[string]) { + "run": func(e *CallbackReference[string]) { panic(panicMsg) }, }, @@ -616,11 +616,11 @@ func TestNoDeadLock(t *testing.T) { var fsm *FSM[string] fsm = NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "run": func(e *Event[string]) { + "run": func(e *CallbackReference[string]) { fsm.Current() // Should not result in a panic / deadlock }, }, @@ -634,11 +634,11 @@ func TestNoDeadLock(t *testing.T) { func TestThreadSafetyRaceCondition(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "run": func(e *Event[string]) { + "run": func(e *CallbackReference[string]) { }, }, ) @@ -661,11 +661,11 @@ func TestDoubleTransition(t *testing.T) { wg.Add(2) fsm = NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "end"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string]{ - "before_run": func(e *Event[string]) { + "before_run": func(e *CallbackReference[string]) { wg.Done() // Imagine a concurrent event coming in of the same type while // the data access mutex is unlocked because the current transition @@ -697,8 +697,8 @@ func TestDoubleTransition(t *testing.T) { func TestNoTransition(t *testing.T) { fsm := NewFSM( "start", - Events{ - {Name: "run", Src: []string{"start"}, Dst: "start"}, + StateMachine[string]{ + {Event: "run", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string]{}, ) @@ -711,36 +711,36 @@ func TestNoTransition(t *testing.T) { func ExampleNewFSM() { fsm := NewFSM( "green", - Events{ - {Name: "warn", Src: []string{"green"}, Dst: "yellow"}, - {Name: "panic", Src: []string{"yellow"}, Dst: "red"}, - {Name: "panic", Src: []string{"green"}, Dst: "red"}, - {Name: "calm", Src: []string{"red"}, Dst: "yellow"}, - {Name: "clear", Src: []string{"yellow"}, Dst: "green"}, + StateMachine[string]{ + {Event: "warn", Src: []string{"green"}, Dst: "yellow"}, + {Event: "panic", Src: []string{"yellow"}, Dst: "red"}, + {Event: "panic", Src: []string{"green"}, Dst: "red"}, + {Event: "calm", Src: []string{"red"}, Dst: "yellow"}, + {Event: "clear", Src: []string{"yellow"}, Dst: "green"}, }, Callbacks[string]{ - "before_warn": func(e *Event[string]) { + "before_warn": func(e *CallbackReference[string]) { fmt.Println("before_warn") }, - "before_event": func(e *Event[string]) { + "before_event": func(e *CallbackReference[string]) { fmt.Println("before_event") }, - "leave_green": func(e *Event[string]) { + "leave_green": func(e *CallbackReference[string]) { fmt.Println("leave_green") }, - "leave_state": func(e *Event[string]) { + "leave_state": func(e *CallbackReference[string]) { fmt.Println("leave_state") }, - "enter_yellow": func(e *Event[string]) { + "enter_yellow": func(e *CallbackReference[string]) { fmt.Println("enter_yellow") }, - "enter_state": func(e *Event[string]) { + "enter_state": func(e *CallbackReference[string]) { fmt.Println("enter_state") }, - "after_warn": func(e *Event[string]) { + "after_warn": func(e *CallbackReference[string]) { fmt.Println("after_warn") }, - "after_event": func(e *Event[string]) { + "after_event": func(e *CallbackReference[string]) { fmt.Println("after_event") }, }, @@ -767,9 +767,9 @@ func ExampleNewFSM() { func ExampleFSM_Current() { fsm := NewFSM( "closed", - Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, + StateMachine[string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, Callbacks[string]{}, ) @@ -780,9 +780,9 @@ func ExampleFSM_Current() { func ExampleFSM_Is() { fsm := NewFSM( "closed", - Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, + StateMachine[string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, Callbacks[string]{}, ) @@ -796,9 +796,9 @@ func ExampleFSM_Is() { func ExampleFSM_Can() { fsm := NewFSM( "closed", - Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, + StateMachine[string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, Callbacks[string]{}, ) @@ -812,10 +812,10 @@ func ExampleFSM_Can() { func ExampleFSM_AvailableTransitions() { fsm := NewFSM( "closed", - Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, - {Name: "kick", Src: []string{"closed"}, Dst: "broken"}, + StateMachine[string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, + {Event: "kick", Src: []string{"closed"}, Dst: "broken"}, }, Callbacks[string]{}, ) @@ -830,9 +830,9 @@ func ExampleFSM_AvailableTransitions() { func ExampleFSM_Cannot() { fsm := NewFSM( "closed", - Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, + StateMachine[string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, Callbacks[string]{}, ) @@ -846,9 +846,9 @@ func ExampleFSM_Cannot() { func ExampleFSM_Event() { fsm := NewFSM( "closed", - Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, + StateMachine[string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, Callbacks[string]{}, ) @@ -872,12 +872,12 @@ func ExampleFSM_Event() { func ExampleFSM_Transition() { fsm := NewFSM( "closed", - Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, + StateMachine[string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, Callbacks[string]{ - "leave_closed": func(e *Event[string]) { + "leave_closed": func(e *CallbackReference[string]) { e.Async() }, }, @@ -896,3 +896,36 @@ func ExampleFSM_Transition() { // closed // open } + +type MyEvent string + +const ( + Close MyEvent = "close" + Open MyEvent = "open" +) + +func ExampleFSM_Event_Generic() { + fsm := NewFSM( + "closed", + StateMachine[MyEvent]{ + {Event: Open, Src: []string{"closed"}, Dst: "open"}, + {Event: Close, Src: []string{"open"}, Dst: "closed"}, + }, + Callbacks[MyEvent]{}, + ) + fmt.Println(fsm.Current()) + err := fsm.Event(Open) + if err != nil { + fmt.Println(err) + } + fmt.Println(fsm.Current()) + err = fsm.Event(Close) + if err != nil { + fmt.Println(err) + } + fmt.Println(fsm.Current()) + // Output: + // closed + // open + // closed +} diff --git a/graphviz_visualizer.go b/graphviz_visualizer.go index d6bc20e..e1c8fbe 100644 --- a/graphviz_visualizer.go +++ b/graphviz_visualizer.go @@ -6,7 +6,7 @@ import ( ) // Visualize outputs a visualization of a FSM in Graphviz format. -func Visualize[E FSMEvent](fsm *FSM[E]) string { +func Visualize[E Event](fsm *FSM[E]) string { var buf bytes.Buffer // we sort the key alphabetically to have a reproducible graph output @@ -26,7 +26,7 @@ func writeHeaderLine(buf *bytes.Buffer) { buf.WriteString("\n") } -func writeTransitions(buf *bytes.Buffer, current string, sortedEKeys []eKey, transitions map[eKey]string) { +func writeTransitions[E Event](buf *bytes.Buffer, current string, sortedEKeys []eKey[E], transitions map[eKey[E]]string) { // make sure the current state is at top for _, k := range sortedEKeys { if k.src == current { diff --git a/graphviz_visualizer_test.go b/graphviz_visualizer_test.go index e8d004a..366f970 100644 --- a/graphviz_visualizer_test.go +++ b/graphviz_visualizer_test.go @@ -9,10 +9,10 @@ import ( func TestGraphvizOutput(t *testing.T) { fsmUnderTest := NewFSM( "closed", - Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, - {Name: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, + StateMachine[string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, + {Event: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, }, Callbacks[string]{}, ) diff --git a/mermaid_visualizer.go b/mermaid_visualizer.go index d62985a..22bc424 100644 --- a/mermaid_visualizer.go +++ b/mermaid_visualizer.go @@ -18,7 +18,7 @@ const ( ) // VisualizeForMermaidWithGraphType outputs a visualization of a FSM in Mermaid format as specified by the graphType. -func VisualizeForMermaidWithGraphType[E FSMEvent](fsm *FSM[E], graphType MermaidDiagramType) (string, error) { +func VisualizeForMermaidWithGraphType[E Event](fsm *FSM[E], graphType MermaidDiagramType) (string, error) { switch graphType { case FlowChart: return visualizeForMermaidAsFlowChart(fsm), nil @@ -29,7 +29,7 @@ func VisualizeForMermaidWithGraphType[E FSMEvent](fsm *FSM[E], graphType Mermaid } } -func visualizeForMermaidAsStateDiagram[E FSMEvent](fsm *FSM[E]) string { +func visualizeForMermaidAsStateDiagram[E Event](fsm *FSM[E]) string { var buf bytes.Buffer sortedTransitionKeys := getSortedTransitionKeys(fsm.transitions) @@ -47,7 +47,7 @@ func visualizeForMermaidAsStateDiagram[E FSMEvent](fsm *FSM[E]) string { } // visualizeForMermaidAsFlowChart outputs a visualization of a FSM in Mermaid format (including highlighting of current state). -func visualizeForMermaidAsFlowChart[E FSMEvent](fsm *FSM[E]) string { +func visualizeForMermaidAsFlowChart[E Event](fsm *FSM[E]) string { var buf bytes.Buffer sortedTransitionKeys := getSortedTransitionKeys(fsm.transitions) @@ -74,7 +74,7 @@ func writeFlowChartStates(buf *bytes.Buffer, sortedStates []string, statesToIDMa buf.WriteString("\n") } -func writeFlowChartTransitions(buf *bytes.Buffer, transitions map[eKey]string, sortedTransitionKeys []eKey, statesToIDMap map[string]string) { +func writeFlowChartTransitions[E Event](buf *bytes.Buffer, transitions map[eKey[E]]string, sortedTransitionKeys []eKey[E], statesToIDMap map[string]string) { for _, transition := range sortedTransitionKeys { target := transitions[transition] buf.WriteString(fmt.Sprintf(` %s --> |%s| %s`, statesToIDMap[transition.src], transition.event, statesToIDMap[target])) diff --git a/mermaid_visualizer_test.go b/mermaid_visualizer_test.go index d391b22..71e7510 100644 --- a/mermaid_visualizer_test.go +++ b/mermaid_visualizer_test.go @@ -9,10 +9,10 @@ import ( func TestMermaidOutput(t *testing.T) { fsmUnderTest := NewFSM( "closed", - Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, - {Name: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, + StateMachine[string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, + {Event: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, }, Callbacks[string]{}, ) @@ -40,12 +40,12 @@ stateDiagram-v2 func TestMermaidFlowChartOutput(t *testing.T) { fsmUnderTest := NewFSM( "closed", - Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "part-open", Src: []string{"closed"}, Dst: "intermediate"}, - {Name: "part-open", Src: []string{"intermediate"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, - {Name: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, + StateMachine[string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "part-open", Src: []string{"closed"}, Dst: "intermediate"}, + {Event: "part-open", Src: []string{"intermediate"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, + {Event: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, }, Callbacks[string]{}, ) diff --git a/visualizer.go b/visualizer.go index a16d4fe..09042fc 100644 --- a/visualizer.go +++ b/visualizer.go @@ -21,7 +21,7 @@ const ( // VisualizeWithType outputs a visualization of a FSM in the desired format. // If the type is not given it defaults to GRAPHVIZ -func VisualizeWithType[E FSMEvent](fsm *FSM[E], visualizeType VisualizeType) (string, error) { +func VisualizeWithType[E Event](fsm *FSM[E], visualizeType VisualizeType) (string, error) { switch visualizeType { case GRAPHVIZ: return Visualize(fsm), nil @@ -36,9 +36,9 @@ func VisualizeWithType[E FSMEvent](fsm *FSM[E], visualizeType VisualizeType) (st } } -func getSortedTransitionKeys(transitions map[eKey]string) []eKey { +func getSortedTransitionKeys[E Event](transitions map[eKey[E]]string) []eKey[E] { // we sort the key alphabetically to have a reproducible graph output - sortedTransitionKeys := make([]eKey, 0) + sortedTransitionKeys := make([]eKey[E], 0) for transition := range transitions { sortedTransitionKeys = append(sortedTransitionKeys, transition) @@ -53,7 +53,7 @@ func getSortedTransitionKeys(transitions map[eKey]string) []eKey { return sortedTransitionKeys } -func getSortedStates(transitions map[eKey]string) ([]string, map[string]string) { +func getSortedStates[E Event](transitions map[eKey[E]]string) ([]string, map[string]string) { statesToIDMap := make(map[string]string) for transition, target := range transitions { if _, ok := statesToIDMap[transition.src]; !ok { From 21ed7a8e0b97c4f5b6d30e7d9bc94dd3dc5b53dc Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 09:53:35 +0200 Subject: [PATCH 03/33] Generic state and Event --- event.go | 20 +-- fsm.go | 114 ++++++++--------- fsm_test.go | 238 ++++++++++++++++++------------------ go.mod | 2 + graphviz_visualizer.go | 6 +- graphviz_visualizer_test.go | 4 +- mermaid_visualizer.go | 12 +- mermaid_visualizer_test.go | 8 +- visualizer.go | 17 +-- 9 files changed, 218 insertions(+), 203 deletions(-) diff --git a/event.go b/event.go index 052373b..e4cd273 100644 --- a/event.go +++ b/event.go @@ -17,20 +17,26 @@ package fsm type Event interface { ~string } +type State interface { + ~string +} +type EventOrState interface { + Event | State +} // CallbackReference is the info that get passed as a reference in the callbacks. -type CallbackReference[E Event] struct { +type CallbackReference[E Event, S State] struct { // fsm is an reference to the current fsm. - fsm *FSM[E] + fsm *FSM[E, S] // Event is the event name. - Event string + Event E // Src is the state before the transition. - Src string + Src S // Dst is the state after the transition. - Dst string + Dst S // Err is an optional error that can be returned from a callback. Err error @@ -48,7 +54,7 @@ type CallbackReference[E Event] struct { // Cancel can be called in before_ or leave_ to cancel the // current transition before it happens. It takes an optional error, which will // overwrite e.Err if set before. -func (e *CallbackReference[E]) Cancel(err ...error) { +func (e *CallbackReference[E, S]) Cancel(err ...error) { e.canceled = true if len(err) > 0 { @@ -61,6 +67,6 @@ func (e *CallbackReference[E]) Cancel(err ...error) { // The current state transition will be on hold in the old state until a final // call to Transition is made. This will complete the transition and possibly // call the other callbacks. -func (e *CallbackReference[E]) Async() { +func (e *CallbackReference[E, S]) Async() { e.async = true } diff --git a/fsm.go b/fsm.go index ced2756..2fa5620 100644 --- a/fsm.go +++ b/fsm.go @@ -30,28 +30,28 @@ import ( ) // transitioner is an interface for the FSM's transition function. -type transitioner[E Event] interface { - transition(*FSM[E]) error +type transitioner[E Event, S State] interface { + transition(*FSM[E, S]) error } // FSM is the state machine that holds the current state. // // It has to be created with NewFSM to function properly. -type FSM[E Event] struct { +type FSM[E Event, S State] struct { // current is the state that the FSM is currently in. - current string + current S // transitions maps events and source states to destination states. - transitions map[eKey[E]]string + transitions map[eKey[E, S]]S // callbacks maps events and targets to callback functions. - callbacks map[cKey]Callback[E] + callbacks map[cKey[E]]Callback[E, S] // transition is the internal transition functions used either directly // or when Transition is called in an asynchronous state transition. transition func() // transitionerObj calls the FSM's transition() function. - transitionerObj transitioner[E] + transitionerObj transitioner[E, S] // stateMu guards access to the current state. stateMu sync.RWMutex @@ -69,28 +69,28 @@ type FSM[E Event] struct { // The event can have one or more source states that is valid for performing // the transition. If the FSM is in one of the source states it will end up in // the specified destination state, calling all defined callbacks as it goes. -type StateMachineEntry[E Event] struct { +type StateMachineEntry[E Event, S State] struct { // Event is the event name used when calling for a transition. Event E // Src is a slice of source states that the FSM must be in to perform a // state transition. - Src []string + Src []S // Dst is the destination state that the FSM will be in if the transition // succeeds. - Dst string + Dst S } // Callback is a function type that callbacks should use. Event is the current // event info as the callback happens. -type Callback[E Event] func(*CallbackReference[E]) +type Callback[E Event, S State] func(*CallbackReference[E, S]) // StateMachine is a shorthand for defining the transition map in NewFSM. -type StateMachine[E Event] []StateMachineEntry[E] +type StateMachine[E Event, S State] []StateMachineEntry[E, S] // Callbacks is a shorthand for defining the callbacks in NewFSM. -type Callbacks[E Event] map[string]Callback[E] +type Callbacks[E Event, S State] map[string]Callback[E, S] // NewFSM constructs a FSM from events and callbacks. // @@ -128,21 +128,21 @@ type Callbacks[E Event] map[string]Callback[E] // which version of the callback will end up in the internal map. This is due // to the pseudo random nature of Go maps. No checking for multiple keys is // currently performed. -func NewFSM[E Event](initial string, events []StateMachineEntry[E], callbacks map[string]Callback[E]) *FSM[E] { - f := &FSM[E]{ - transitionerObj: &transitionerStruct[E]{}, +func NewFSM[E Event, S State](initial S, events []StateMachineEntry[E, S], callbacks map[string]Callback[E, S]) *FSM[E, S] { + f := &FSM[E, S]{ + transitionerObj: &transitionerStruct[E, S]{}, current: initial, - transitions: make(map[eKey[E]]string), - callbacks: make(map[cKey]Callback[E]), + transitions: make(map[eKey[E, S]]S), + callbacks: make(map[cKey[E]]Callback[E, S]), metadata: make(map[string]interface{}), } // Build transition map and store sets of all events and states. allEvents := make(map[E]bool) - allStates := make(map[string]bool) + allStates := make(map[S]bool) for _, e := range events { for _, src := range e.Src { - f.transitions[eKey[E]{e.Event, src}] = e.Dst + f.transitions[eKey[E, S]{e.Event, src}] = e.Dst allStates[src] = true allStates[e.Dst] = true } @@ -168,7 +168,7 @@ func NewFSM[E Event](initial string, events []StateMachineEntry[E], callbacks ma if target == "state" { target = "" callbackType = callbackLeaveState - } else if _, ok := allStates[target]; ok { + } else if _, ok := allStates[S(target)]; ok { callbackType = callbackLeaveState } case strings.HasPrefix(name, "enter_"): @@ -176,7 +176,7 @@ func NewFSM[E Event](initial string, events []StateMachineEntry[E], callbacks ma if target == "state" { target = "" callbackType = callbackEnterState - } else if _, ok := allStates[target]; ok { + } else if _, ok := allStates[S(target)]; ok { callbackType = callbackEnterState } case strings.HasPrefix(name, "after_"): @@ -189,7 +189,7 @@ func NewFSM[E Event](initial string, events []StateMachineEntry[E], callbacks ma } default: target = name - if _, ok := allStates[target]; ok { + if _, ok := allStates[S(target)]; ok { callbackType = callbackEnterState } else if _, ok := allEvents[E(target)]; ok { // FIXME callbackType = callbackAfterEvent @@ -197,7 +197,7 @@ func NewFSM[E Event](initial string, events []StateMachineEntry[E], callbacks ma } if callbackType != callbackNone { - f.callbacks[cKey{target, callbackType}] = fn + f.callbacks[cKey[E]{E(target), callbackType}] = fn // FIXME } } @@ -205,14 +205,14 @@ func NewFSM[E Event](initial string, events []StateMachineEntry[E], callbacks ma } // Current returns the current state of the FSM. -func (f *FSM[E]) Current() string { +func (f *FSM[E, S]) Current() S { f.stateMu.RLock() defer f.stateMu.RUnlock() return f.current } // Is returns true if state is the current state. -func (f *FSM[E]) Is(state string) bool { +func (f *FSM[E, S]) Is(state S) bool { f.stateMu.RLock() defer f.stateMu.RUnlock() return state == f.current @@ -220,23 +220,23 @@ func (f *FSM[E]) Is(state string) bool { // SetState allows the user to move to the given state from current state. // The call does not trigger any callbacks, if defined. -func (f *FSM[E]) SetState(state string) { +func (f *FSM[E, S]) SetState(state S) { f.stateMu.Lock() defer f.stateMu.Unlock() f.current = state } // Can returns true if event can occur in the current state. -func (f *FSM[E]) Can(event E) bool { +func (f *FSM[E, S]) Can(event E) bool { f.stateMu.RLock() defer f.stateMu.RUnlock() - _, ok := f.transitions[eKey[E]{event, f.current}] + _, ok := f.transitions[eKey[E, S]{event, f.current}] return ok && (f.transition == nil) } // AvailableTransitions returns a list of transitions available in the // current state. -func (f *FSM[E]) AvailableTransitions() []string { +func (f *FSM[E, S]) AvailableTransitions() []string { f.stateMu.RLock() defer f.stateMu.RUnlock() var transitions []string @@ -250,12 +250,12 @@ func (f *FSM[E]) AvailableTransitions() []string { // Cannot returns true if event can not occur in the current state. // It is a convenience method to help code read nicely. -func (f *FSM[E]) Cannot(event E) bool { +func (f *FSM[E, S]) Cannot(event E) bool { return !f.Can(event) } // Metadata returns the value stored in metadata -func (f *FSM[E]) Metadata(key string) (interface{}, bool) { +func (f *FSM[E, S]) Metadata(key string) (interface{}, bool) { f.metadataMu.RLock() defer f.metadataMu.RUnlock() dataElement, ok := f.metadata[key] @@ -263,7 +263,7 @@ func (f *FSM[E]) Metadata(key string) (interface{}, bool) { } // SetMetadata stores the dataValue in metadata indexing it with key -func (f *FSM[E]) SetMetadata(key string, dataValue interface{}) { +func (f *FSM[E, S]) SetMetadata(key string, dataValue interface{}) { f.metadataMu.Lock() defer f.metadataMu.Unlock() f.metadata[key] = dataValue @@ -286,7 +286,7 @@ func (f *FSM[E]) SetMetadata(key string, dataValue interface{}) { // // The last error should never occur in this situation and is a sign of an // internal bug. -func (f *FSM[E]) Event(event E, args ...interface{}) error { +func (f *FSM[E, S]) Event(event E, args ...interface{}) error { f.eventMu.Lock() defer f.eventMu.Unlock() @@ -297,17 +297,17 @@ func (f *FSM[E]) Event(event E, args ...interface{}) error { return InTransitionError{string(event)} } - dst, ok := f.transitions[eKey[E]{event, f.current}] + dst, ok := f.transitions[eKey[E, S]{event, f.current}] if !ok { for ekey := range f.transitions { if ekey.event == event { - return InvalidEventError{string(event), f.current} + return InvalidEventError{string(event), string(f.current)} } } return UnknownEventError{string(event)} } - e := &CallbackReference[E]{f, string(event), f.current, dst, nil, args, false, false} + e := &CallbackReference[E, S]{f, event, f.current, dst, nil, args, false, false} err := f.beforeEventCallbacks(e) if err != nil { @@ -348,26 +348,26 @@ func (f *FSM[E]) Event(event E, args ...interface{}) error { } // Transition wraps transitioner.transition. -func (f *FSM[E]) Transition() error { +func (f *FSM[E, S]) Transition() error { f.eventMu.Lock() defer f.eventMu.Unlock() return f.doTransition() } // doTransition wraps transitioner.transition. -func (f *FSM[E]) doTransition() error { +func (f *FSM[E, S]) doTransition() error { return f.transitionerObj.transition(f) } // transitionerStruct is the default implementation of the transitioner // interface. Other implementations can be swapped in for testing. -type transitionerStruct[E Event] struct{} +type transitionerStruct[E Event, S State] struct{} // Transition completes an asynchronous state change. // // The callback for leave_ must previously have called Async on its // event to have initiated an asynchronous state transition. -func (t transitionerStruct[E]) transition(f *FSM[E]) error { +func (t transitionerStruct[E, S]) transition(f *FSM[E, S]) error { if f.transition == nil { return NotInTransitionError{} } @@ -378,14 +378,14 @@ func (t transitionerStruct[E]) transition(f *FSM[E]) error { // beforeEventCallbacks calls the before_ callbacks, first the named then the // general version. -func (f *FSM[E]) beforeEventCallbacks(e *CallbackReference[E]) error { - if fn, ok := f.callbacks[cKey{e.Event, callbackBeforeEvent}]; ok { +func (f *FSM[E, S]) beforeEventCallbacks(e *CallbackReference[E, S]) error { + if fn, ok := f.callbacks[cKey[E]{e.Event, callbackBeforeEvent}]; ok { fn(e) if e.canceled { return CanceledError{e.Err} } } - if fn, ok := f.callbacks[cKey{"", callbackBeforeEvent}]; ok { + if fn, ok := f.callbacks[cKey[E]{"", callbackBeforeEvent}]; ok { fn(e) if e.canceled { return CanceledError{e.Err} @@ -396,8 +396,8 @@ func (f *FSM[E]) beforeEventCallbacks(e *CallbackReference[E]) error { // leaveStateCallbacks calls the leave_ callbacks, first the named then the // general version. -func (f *FSM[E]) leaveStateCallbacks(e *CallbackReference[E]) error { - if fn, ok := f.callbacks[cKey{f.current, callbackLeaveState}]; ok { +func (f *FSM[E, S]) leaveStateCallbacks(e *CallbackReference[E, S]) error { + if fn, ok := f.callbacks[cKey[E]{E(f.current), callbackLeaveState}]; ok { // FIXME fn(e) if e.canceled { return CanceledError{e.Err} @@ -405,7 +405,7 @@ func (f *FSM[E]) leaveStateCallbacks(e *CallbackReference[E]) error { return AsyncError{e.Err} } } - if fn, ok := f.callbacks[cKey{"", callbackLeaveState}]; ok { + if fn, ok := f.callbacks[cKey[E]{"", callbackLeaveState}]; ok { fn(e) if e.canceled { return CanceledError{e.Err} @@ -418,22 +418,22 @@ func (f *FSM[E]) leaveStateCallbacks(e *CallbackReference[E]) error { // enterStateCallbacks calls the enter_ callbacks, first the named then the // general version. -func (f *FSM[E]) enterStateCallbacks(e *CallbackReference[E]) { - if fn, ok := f.callbacks[cKey{f.current, callbackEnterState}]; ok { +func (f *FSM[E, S]) enterStateCallbacks(e *CallbackReference[E, S]) { + if fn, ok := f.callbacks[cKey[E]{E(f.current), callbackEnterState}]; ok { // FIXME fn(e) } - if fn, ok := f.callbacks[cKey{"", callbackEnterState}]; ok { + if fn, ok := f.callbacks[cKey[E]{"", callbackEnterState}]; ok { fn(e) } } // afterEventCallbacks calls the after_ callbacks, first the named then the // general version. -func (f *FSM[E]) afterEventCallbacks(e *CallbackReference[E]) { - if fn, ok := f.callbacks[cKey{e.Event, callbackAfterEvent}]; ok { +func (f *FSM[E, S]) afterEventCallbacks(e *CallbackReference[E, S]) { + if fn, ok := f.callbacks[cKey[E]{e.Event, callbackAfterEvent}]; ok { fn(e) } - if fn, ok := f.callbacks[cKey{"", callbackAfterEvent}]; ok { + if fn, ok := f.callbacks[cKey[E]{"", callbackAfterEvent}]; ok { fn(e) } } @@ -447,21 +447,21 @@ const ( ) // cKey is a struct key used for keeping the callbacks mapped to a target. -type cKey struct { +type cKey[ES EventOrState] struct { // target is either the name of a state or an event depending on which // callback type the key refers to. It can also be "" for a non-targeted // callback like before_event. - target string + target ES // callbackType is the situation when the callback will be run. callbackType int } // eKey is a struct key used for storing the transition map. -type eKey[E Event] struct { +type eKey[E Event, S State] struct { // event is the name of the event that the keys refers to. event E // src is the source from where the event can transition. - src string + src S } diff --git a/fsm_test.go b/fsm_test.go index 64e3939..bcdd256 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -22,20 +22,20 @@ import ( "time" ) -type fakeTransitionerObj[E Event] struct { +type fakeTransitionerObj[E Event, S State] struct { } -func (t fakeTransitionerObj[E]) transition(f *FSM[E]) error { +func (t fakeTransitionerObj[E, S]) transition(f *FSM[E, S]) error { return &InternalError{} } func TestSameState(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "start"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) _ = fsm.Event("run") if fsm.Current() != "start" { @@ -46,10 +46,10 @@ func TestSameState(t *testing.T) { func TestSetState(t *testing.T) { fsm := NewFSM( "walking", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "walk", Src: []string{"start"}, Dst: "walking"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) fsm.SetState("start") if fsm.Current() != "start" { @@ -64,12 +64,12 @@ func TestSetState(t *testing.T) { func TestBadTransition(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "running"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) - fsm.transitionerObj = new(fakeTransitionerObj[string]) + fsm.transitionerObj = new(fakeTransitionerObj[string, string]) err := fsm.Event("run") if err == nil { t.Error("bad transition should give an error") @@ -79,11 +79,11 @@ func TestBadTransition(t *testing.T) { func TestInappropriateEvent(t *testing.T) { fsm := NewFSM( "closed", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) err := fsm.Event("close") if e, ok := err.(InvalidEventError); !ok && e.Event != "close" && e.State != "closed" { @@ -94,11 +94,11 @@ func TestInappropriateEvent(t *testing.T) { func TestInvalidEvent(t *testing.T) { fsm := NewFSM( "closed", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) err := fsm.Event("lock") if e, ok := err.(UnknownEventError); !ok && e.Event != "close" { @@ -109,12 +109,12 @@ func TestInvalidEvent(t *testing.T) { func TestMultipleSources(t *testing.T) { fsm := NewFSM( "one", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "first", Src: []string{"one"}, Dst: "two"}, {Event: "second", Src: []string{"two"}, Dst: "three"}, {Event: "reset", Src: []string{"one", "two", "three"}, Dst: "one"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) err := fsm.Event("first") @@ -154,14 +154,14 @@ func TestMultipleSources(t *testing.T) { func TestMultipleEvents(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "first", Src: []string{"start"}, Dst: "one"}, {Event: "second", Src: []string{"start"}, Dst: "two"}, {Event: "reset", Src: []string{"one"}, Dst: "reset_one"}, {Event: "reset", Src: []string{"two"}, Dst: "reset_two"}, {Event: "reset", Src: []string{"reset_one", "reset_two"}, Dst: "start"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) err := fsm.Event("first") @@ -211,20 +211,20 @@ func TestGenericCallbacks(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "before_event": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "before_event": func(e *CallbackReference[string, string]) { beforeEvent = true }, - "leave_state": func(e *CallbackReference[string]) { + "leave_state": func(e *CallbackReference[string, string]) { leaveState = true }, - "enter_state": func(e *CallbackReference[string]) { + "enter_state": func(e *CallbackReference[string, string]) { enterState = true }, - "after_event": func(e *CallbackReference[string]) { + "after_event": func(e *CallbackReference[string, string]) { afterEvent = true }, }, @@ -247,20 +247,20 @@ func TestSpecificCallbacks(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "before_run": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "before_run": func(e *CallbackReference[string, string]) { beforeEvent = true }, - "leave_start": func(e *CallbackReference[string]) { + "leave_start": func(e *CallbackReference[string, string]) { leaveState = true }, - "enter_end": func(e *CallbackReference[string]) { + "enter_end": func(e *CallbackReference[string, string]) { enterState = true }, - "after_run": func(e *CallbackReference[string]) { + "after_run": func(e *CallbackReference[string, string]) { afterEvent = true }, }, @@ -281,14 +281,14 @@ func TestSpecificCallbacksShortform(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "end": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "end": func(e *CallbackReference[string, string]) { enterState = true }, - "run": func(e *CallbackReference[string]) { + "run": func(e *CallbackReference[string, string]) { afterEvent = true }, }, @@ -308,11 +308,11 @@ func TestBeforeEventWithoutTransition(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "dontrun", Src: []string{"start"}, Dst: "start"}, }, - Callbacks[string]{ - "before_event": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "before_event": func(e *CallbackReference[string, string]) { beforeEvent = true }, }, @@ -334,11 +334,11 @@ func TestBeforeEventWithoutTransition(t *testing.T) { func TestCancelBeforeGenericEvent(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "before_event": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "before_event": func(e *CallbackReference[string, string]) { e.Cancel() }, }, @@ -352,11 +352,11 @@ func TestCancelBeforeGenericEvent(t *testing.T) { func TestCancelBeforeSpecificEvent(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "before_run": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "before_run": func(e *CallbackReference[string, string]) { e.Cancel() }, }, @@ -370,11 +370,11 @@ func TestCancelBeforeSpecificEvent(t *testing.T) { func TestCancelLeaveGenericState(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "leave_state": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "leave_state": func(e *CallbackReference[string, string]) { e.Cancel() }, }, @@ -388,11 +388,11 @@ func TestCancelLeaveGenericState(t *testing.T) { func TestCancelLeaveSpecificState(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "leave_start": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "leave_start": func(e *CallbackReference[string, string]) { e.Cancel() }, }, @@ -406,11 +406,11 @@ func TestCancelLeaveSpecificState(t *testing.T) { func TestCancelWithError(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "before_event": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "before_event": func(e *CallbackReference[string, string]) { e.Cancel(fmt.Errorf("error")) }, }, @@ -432,11 +432,11 @@ func TestCancelWithError(t *testing.T) { func TestAsyncTransitionGenericState(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "leave_state": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "leave_state": func(e *CallbackReference[string, string]) { e.Async() }, }, @@ -457,11 +457,11 @@ func TestAsyncTransitionGenericState(t *testing.T) { func TestAsyncTransitionSpecificState(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "leave_start": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "leave_start": func(e *CallbackReference[string, string]) { e.Async() }, }, @@ -482,12 +482,12 @@ func TestAsyncTransitionSpecificState(t *testing.T) { func TestAsyncTransitionInProgress(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, {Event: "reset", Src: []string{"end"}, Dst: "start"}, }, - Callbacks[string]{ - "leave_start": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "leave_start": func(e *CallbackReference[string, string]) { e.Async() }, }, @@ -513,11 +513,11 @@ func TestAsyncTransitionInProgress(t *testing.T) { func TestAsyncTransitionNotInProgress(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, {Event: "reset", Src: []string{"end"}, Dst: "start"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) err := fsm.Transition() if _, ok := err.(NotInTransitionError); !ok { @@ -528,11 +528,11 @@ func TestAsyncTransitionNotInProgress(t *testing.T) { func TestCallbackNoError(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "run": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "run": func(e *CallbackReference[string, string]) { }, }, ) @@ -545,11 +545,11 @@ func TestCallbackNoError(t *testing.T) { func TestCallbackError(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "run": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "run": func(e *CallbackReference[string, string]) { e.Err = fmt.Errorf("error") }, }, @@ -563,11 +563,11 @@ func TestCallbackError(t *testing.T) { func TestCallbackArgs(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "run": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "run": func(e *CallbackReference[string, string]) { if len(e.Args) != 1 { t.Error("too few arguments") } @@ -597,11 +597,11 @@ func TestCallbackPanic(t *testing.T) { }() fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "run": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "run": func(e *CallbackReference[string, string]) { panic(panicMsg) }, }, @@ -613,14 +613,14 @@ func TestCallbackPanic(t *testing.T) { } func TestNoDeadLock(t *testing.T) { - var fsm *FSM[string] + var fsm *FSM[string, string] fsm = NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "run": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "run": func(e *CallbackReference[string, string]) { fsm.Current() // Should not result in a panic / deadlock }, }, @@ -634,11 +634,11 @@ func TestNoDeadLock(t *testing.T) { func TestThreadSafetyRaceCondition(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "run": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "run": func(e *CallbackReference[string, string]) { }, }, ) @@ -656,16 +656,16 @@ func TestThreadSafetyRaceCondition(t *testing.T) { } func TestDoubleTransition(t *testing.T) { - var fsm *FSM[string] + var fsm *FSM[string, string] var wg sync.WaitGroup wg.Add(2) fsm = NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, - Callbacks[string]{ - "before_run": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "before_run": func(e *CallbackReference[string, string]) { wg.Done() // Imagine a concurrent event coming in of the same type while // the data access mutex is unlocked because the current transition @@ -697,10 +697,10 @@ func TestDoubleTransition(t *testing.T) { func TestNoTransition(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "start"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) err := fsm.Event("run") if _, ok := err.(NoTransitionError); !ok { @@ -711,36 +711,36 @@ func TestNoTransition(t *testing.T) { func ExampleNewFSM() { fsm := NewFSM( "green", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "warn", Src: []string{"green"}, Dst: "yellow"}, {Event: "panic", Src: []string{"yellow"}, Dst: "red"}, {Event: "panic", Src: []string{"green"}, Dst: "red"}, {Event: "calm", Src: []string{"red"}, Dst: "yellow"}, {Event: "clear", Src: []string{"yellow"}, Dst: "green"}, }, - Callbacks[string]{ - "before_warn": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "before_warn": func(e *CallbackReference[string, string]) { fmt.Println("before_warn") }, - "before_event": func(e *CallbackReference[string]) { + "before_event": func(e *CallbackReference[string, string]) { fmt.Println("before_event") }, - "leave_green": func(e *CallbackReference[string]) { + "leave_green": func(e *CallbackReference[string, string]) { fmt.Println("leave_green") }, - "leave_state": func(e *CallbackReference[string]) { + "leave_state": func(e *CallbackReference[string, string]) { fmt.Println("leave_state") }, - "enter_yellow": func(e *CallbackReference[string]) { + "enter_yellow": func(e *CallbackReference[string, string]) { fmt.Println("enter_yellow") }, - "enter_state": func(e *CallbackReference[string]) { + "enter_state": func(e *CallbackReference[string, string]) { fmt.Println("enter_state") }, - "after_warn": func(e *CallbackReference[string]) { + "after_warn": func(e *CallbackReference[string, string]) { fmt.Println("after_warn") }, - "after_event": func(e *CallbackReference[string]) { + "after_event": func(e *CallbackReference[string, string]) { fmt.Println("after_event") }, }, @@ -767,11 +767,11 @@ func ExampleNewFSM() { func ExampleFSM_Current() { fsm := NewFSM( "closed", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) fmt.Println(fsm.Current()) // Output: closed @@ -780,11 +780,11 @@ func ExampleFSM_Current() { func ExampleFSM_Is() { fsm := NewFSM( "closed", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) fmt.Println(fsm.Is("closed")) fmt.Println(fsm.Is("open")) @@ -796,11 +796,11 @@ func ExampleFSM_Is() { func ExampleFSM_Can() { fsm := NewFSM( "closed", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) fmt.Println(fsm.Can("open")) fmt.Println(fsm.Can("close")) @@ -812,12 +812,12 @@ func ExampleFSM_Can() { func ExampleFSM_AvailableTransitions() { fsm := NewFSM( "closed", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, {Event: "kick", Src: []string{"closed"}, Dst: "broken"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) // sort the results ordering is consistent for the output checker transitions := fsm.AvailableTransitions() @@ -830,11 +830,11 @@ func ExampleFSM_AvailableTransitions() { func ExampleFSM_Cannot() { fsm := NewFSM( "closed", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) fmt.Println(fsm.Cannot("open")) fmt.Println(fsm.Cannot("close")) @@ -846,11 +846,11 @@ func ExampleFSM_Cannot() { func ExampleFSM_Event() { fsm := NewFSM( "closed", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) fmt.Println(fsm.Current()) err := fsm.Event("open") @@ -872,12 +872,12 @@ func ExampleFSM_Event() { func ExampleFSM_Transition() { fsm := NewFSM( "closed", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, - Callbacks[string]{ - "leave_closed": func(e *CallbackReference[string]) { + Callbacks[string, string]{ + "leave_closed": func(e *CallbackReference[string, string]) { e.Async() }, }, @@ -898,20 +898,24 @@ func ExampleFSM_Transition() { } type MyEvent string +type MyState string const ( Close MyEvent = "close" Open MyEvent = "open" + + IsClosed MyState = "closed" + IsOpen MyState = "open" ) func ExampleFSM_Event_Generic() { fsm := NewFSM( - "closed", - StateMachine[MyEvent]{ - {Event: Open, Src: []string{"closed"}, Dst: "open"}, - {Event: Close, Src: []string{"open"}, Dst: "closed"}, + IsClosed, + StateMachine[MyEvent, MyState]{ + {Event: Open, Src: []MyState{IsClosed}, Dst: IsOpen}, + {Event: Close, Src: []MyState{IsOpen}, Dst: IsClosed}, }, - Callbacks[MyEvent]{}, + Callbacks[MyEvent, MyState]{}, ) fmt.Println(fsm.Current()) err := fsm.Event(Open) diff --git a/go.mod b/go.mod index e3773db..623113b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/looplab/fsm go 1.18 + +require golang.org/x/exp v0.0.0-20220706164943-b4a6d9510983 diff --git a/graphviz_visualizer.go b/graphviz_visualizer.go index e1c8fbe..e00becf 100644 --- a/graphviz_visualizer.go +++ b/graphviz_visualizer.go @@ -6,7 +6,7 @@ import ( ) // Visualize outputs a visualization of a FSM in Graphviz format. -func Visualize[E Event](fsm *FSM[E]) string { +func Visualize[E Event, S State](fsm *FSM[E, S]) string { var buf bytes.Buffer // we sort the key alphabetically to have a reproducible graph output @@ -26,7 +26,7 @@ func writeHeaderLine(buf *bytes.Buffer) { buf.WriteString("\n") } -func writeTransitions[E Event](buf *bytes.Buffer, current string, sortedEKeys []eKey[E], transitions map[eKey[E]]string) { +func writeTransitions[E Event, S State](buf *bytes.Buffer, current S, sortedEKeys []eKey[E, S], transitions map[eKey[E, S]]S) { // make sure the current state is at top for _, k := range sortedEKeys { if k.src == current { @@ -46,7 +46,7 @@ func writeTransitions[E Event](buf *bytes.Buffer, current string, sortedEKeys [] buf.WriteString("\n") } -func writeStates(buf *bytes.Buffer, sortedStateKeys []string) { +func writeStates[S State](buf *bytes.Buffer, sortedStateKeys []S) { for _, k := range sortedStateKeys { buf.WriteString(fmt.Sprintf(` "%s";`, k)) buf.WriteString("\n") diff --git a/graphviz_visualizer_test.go b/graphviz_visualizer_test.go index 366f970..6df6e7f 100644 --- a/graphviz_visualizer_test.go +++ b/graphviz_visualizer_test.go @@ -9,12 +9,12 @@ import ( func TestGraphvizOutput(t *testing.T) { fsmUnderTest := NewFSM( "closed", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, {Event: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) got := Visualize(fsmUnderTest) diff --git a/mermaid_visualizer.go b/mermaid_visualizer.go index 22bc424..e566883 100644 --- a/mermaid_visualizer.go +++ b/mermaid_visualizer.go @@ -18,7 +18,7 @@ const ( ) // VisualizeForMermaidWithGraphType outputs a visualization of a FSM in Mermaid format as specified by the graphType. -func VisualizeForMermaidWithGraphType[E Event](fsm *FSM[E], graphType MermaidDiagramType) (string, error) { +func VisualizeForMermaidWithGraphType[E Event, S State](fsm *FSM[E, S], graphType MermaidDiagramType) (string, error) { switch graphType { case FlowChart: return visualizeForMermaidAsFlowChart(fsm), nil @@ -29,7 +29,7 @@ func VisualizeForMermaidWithGraphType[E Event](fsm *FSM[E], graphType MermaidDia } } -func visualizeForMermaidAsStateDiagram[E Event](fsm *FSM[E]) string { +func visualizeForMermaidAsStateDiagram[E Event, S State](fsm *FSM[E, S]) string { var buf bytes.Buffer sortedTransitionKeys := getSortedTransitionKeys(fsm.transitions) @@ -47,7 +47,7 @@ func visualizeForMermaidAsStateDiagram[E Event](fsm *FSM[E]) string { } // visualizeForMermaidAsFlowChart outputs a visualization of a FSM in Mermaid format (including highlighting of current state). -func visualizeForMermaidAsFlowChart[E Event](fsm *FSM[E]) string { +func visualizeForMermaidAsFlowChart[E Event, S State](fsm *FSM[E, S]) string { var buf bytes.Buffer sortedTransitionKeys := getSortedTransitionKeys(fsm.transitions) @@ -65,7 +65,7 @@ func writeFlowChartGraphType(buf *bytes.Buffer) { buf.WriteString("graph LR\n") } -func writeFlowChartStates(buf *bytes.Buffer, sortedStates []string, statesToIDMap map[string]string) { +func writeFlowChartStates[S State](buf *bytes.Buffer, sortedStates []S, statesToIDMap map[S]string) { for _, state := range sortedStates { buf.WriteString(fmt.Sprintf(` %s[%s]`, statesToIDMap[state], state)) buf.WriteString("\n") @@ -74,7 +74,7 @@ func writeFlowChartStates(buf *bytes.Buffer, sortedStates []string, statesToIDMa buf.WriteString("\n") } -func writeFlowChartTransitions[E Event](buf *bytes.Buffer, transitions map[eKey[E]]string, sortedTransitionKeys []eKey[E], statesToIDMap map[string]string) { +func writeFlowChartTransitions[E Event, S State](buf *bytes.Buffer, transitions map[eKey[E, S]]S, sortedTransitionKeys []eKey[E, S], statesToIDMap map[S]string) { for _, transition := range sortedTransitionKeys { target := transitions[transition] buf.WriteString(fmt.Sprintf(` %s --> |%s| %s`, statesToIDMap[transition.src], transition.event, statesToIDMap[target])) @@ -83,7 +83,7 @@ func writeFlowChartTransitions[E Event](buf *bytes.Buffer, transitions map[eKey[ buf.WriteString("\n") } -func writeFlowChartHighlightCurrent(buf *bytes.Buffer, current string, statesToIDMap map[string]string) { +func writeFlowChartHighlightCurrent[S State](buf *bytes.Buffer, current S, statesToIDMap map[S]string) { buf.WriteString(fmt.Sprintf(` style %s fill:%s`, statesToIDMap[current], highlightingColor)) buf.WriteString("\n") } diff --git a/mermaid_visualizer_test.go b/mermaid_visualizer_test.go index 71e7510..b17cc29 100644 --- a/mermaid_visualizer_test.go +++ b/mermaid_visualizer_test.go @@ -9,12 +9,12 @@ import ( func TestMermaidOutput(t *testing.T) { fsmUnderTest := NewFSM( "closed", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, {Event: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) got, err := VisualizeForMermaidWithGraphType(fsmUnderTest, StateDiagram) @@ -40,14 +40,14 @@ stateDiagram-v2 func TestMermaidFlowChartOutput(t *testing.T) { fsmUnderTest := NewFSM( "closed", - StateMachine[string]{ + StateMachine[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "part-open", Src: []string{"closed"}, Dst: "intermediate"}, {Event: "part-open", Src: []string{"intermediate"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, {Event: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, }, - Callbacks[string]{}, + Callbacks[string, string]{}, ) got, err := VisualizeForMermaidWithGraphType(fsmUnderTest, FlowChart) diff --git a/visualizer.go b/visualizer.go index 09042fc..509565b 100644 --- a/visualizer.go +++ b/visualizer.go @@ -3,6 +3,8 @@ package fsm import ( "fmt" "sort" + + "golang.org/x/exp/slices" ) // VisualizeType the type of the visualization @@ -21,7 +23,7 @@ const ( // VisualizeWithType outputs a visualization of a FSM in the desired format. // If the type is not given it defaults to GRAPHVIZ -func VisualizeWithType[E Event](fsm *FSM[E], visualizeType VisualizeType) (string, error) { +func VisualizeWithType[E Event, S State](fsm *FSM[E, S], visualizeType VisualizeType) (string, error) { switch visualizeType { case GRAPHVIZ: return Visualize(fsm), nil @@ -36,9 +38,9 @@ func VisualizeWithType[E Event](fsm *FSM[E], visualizeType VisualizeType) (strin } } -func getSortedTransitionKeys[E Event](transitions map[eKey[E]]string) []eKey[E] { +func getSortedTransitionKeys[E Event, S State](transitions map[eKey[E, S]]S) []eKey[E, S] { // we sort the key alphabetically to have a reproducible graph output - sortedTransitionKeys := make([]eKey[E], 0) + sortedTransitionKeys := make([]eKey[E, S], 0) for transition := range transitions { sortedTransitionKeys = append(sortedTransitionKeys, transition) @@ -53,8 +55,8 @@ func getSortedTransitionKeys[E Event](transitions map[eKey[E]]string) []eKey[E] return sortedTransitionKeys } -func getSortedStates[E Event](transitions map[eKey[E]]string) ([]string, map[string]string) { - statesToIDMap := make(map[string]string) +func getSortedStates[E Event, S State](transitions map[eKey[E, S]]S) ([]S, map[S]string) { + statesToIDMap := make(map[S]string) for transition, target := range transitions { if _, ok := statesToIDMap[transition.src]; !ok { statesToIDMap[transition.src] = "" @@ -64,11 +66,12 @@ func getSortedStates[E Event](transitions map[eKey[E]]string) ([]string, map[str } } - sortedStates := make([]string, 0, len(statesToIDMap)) + sortedStates := make([]S, 0, len(statesToIDMap)) for state := range statesToIDMap { sortedStates = append(sortedStates, state) } - sort.Strings(sortedStates) + + slices.Sort(sortedStates) for i, state := range sortedStates { statesToIDMap[state] = fmt.Sprintf("id%d", i) From d00aa3c825d2453039af0da4c3b042c36be6711e Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 11:58:32 +0200 Subject: [PATCH 04/33] Adopt examples --- event.go | 4 ++-- examples/alternate.go | 25 ++++++++++++++----------- examples/data.go | 13 +++++++------ examples/simple.go | 10 ++++++---- examples/struct.go | 16 +++++++++------- fsm.go | 2 +- go.sum | 2 ++ 7 files changed, 41 insertions(+), 31 deletions(-) create mode 100644 go.sum diff --git a/event.go b/event.go index e4cd273..f761299 100644 --- a/event.go +++ b/event.go @@ -26,8 +26,8 @@ type EventOrState interface { // CallbackReference is the info that get passed as a reference in the callbacks. type CallbackReference[E Event, S State] struct { - // fsm is an reference to the current fsm. - fsm *FSM[E, S] + // FSM is an reference to the current FSM. + FSM *FSM[E, S] // Event is the event name. Event E diff --git a/examples/alternate.go b/examples/alternate.go index 8de3fe7..9eef8ba 100644 --- a/examples/alternate.go +++ b/examples/alternate.go @@ -1,33 +1,36 @@ +//go:build ignore // +build ignore package main import ( "fmt" + "github.com/looplab/fsm" ) func main() { + fsm := fsm.NewFSM( "idle", - fsm.Events{ - {Name: "scan", Src: []string{"idle"}, Dst: "scanning"}, - {Name: "working", Src: []string{"scanning"}, Dst: "scanning"}, - {Name: "situation", Src: []string{"scanning"}, Dst: "scanning"}, - {Name: "situation", Src: []string{"idle"}, Dst: "idle"}, - {Name: "finish", Src: []string{"scanning"}, Dst: "idle"}, + fsm.StateMachine[string, string]{ + {Event: "scan", Src: []string{"idle"}, Dst: "scanning"}, + {Event: "working", Src: []string{"scanning"}, Dst: "scanning"}, + {Event: "situation", Src: []string{"scanning"}, Dst: "scanning"}, + {Event: "situation", Src: []string{"idle"}, Dst: "idle"}, + {Event: "finish", Src: []string{"scanning"}, Dst: "idle"}, }, - fsm.Callbacks{ - "scan": func(e *fsm.Event) { + fsm.Callbacks[string, string]{ + "scan": func(e *fsm.CallbackReference[string, string]) { fmt.Println("after_scan: " + e.FSM.Current()) }, - "working": func(e *fsm.Event) { + "working": func(e *fsm.CallbackReference[string, string]) { fmt.Println("working: " + e.FSM.Current()) }, - "situation": func(e *fsm.Event) { + "situation": func(e *fsm.CallbackReference[string, string]) { fmt.Println("situation: " + e.FSM.Current()) }, - "finish": func(e *fsm.Event) { + "finish": func(e *fsm.CallbackReference[string, string]) { fmt.Println("finish: " + e.FSM.Current()) }, }, diff --git a/examples/data.go b/examples/data.go index 26aa1f0..3ef1c40 100644 --- a/examples/data.go +++ b/examples/data.go @@ -1,3 +1,4 @@ +//go:build ignore // +build ignore package main @@ -11,16 +12,16 @@ import ( func main() { fsm := fsm.NewFSM( "idle", - fsm.Events{ - {Name: "produce", Src: []string{"idle"}, Dst: "idle"}, - {Name: "consume", Src: []string{"idle"}, Dst: "idle"}, + fsm.StateMachine[string, string]{ + {Event: "produce", Src: []string{"idle"}, Dst: "idle"}, + {Event: "consume", Src: []string{"idle"}, Dst: "idle"}, }, - fsm.Callbacks{ - "produce": func(e *fsm.Event) { + fsm.Callbacks[string, string]{ + "produce": func(e *fsm.CallbackReference[string, string]) { e.FSM.SetMetadata("message", "hii") fmt.Println("produced data") }, - "consume": func(e *fsm.Event) { + "consume": func(e *fsm.CallbackReference[string, string]) { message, ok := e.FSM.Metadata("message") if ok { fmt.Println("message = " + message.(string)) diff --git a/examples/simple.go b/examples/simple.go index 740e4d9..8e16c67 100644 --- a/examples/simple.go +++ b/examples/simple.go @@ -1,20 +1,22 @@ +//go:build ignore // +build ignore package main import ( "fmt" + "github.com/looplab/fsm" ) func main() { fsm := fsm.NewFSM( "closed", - fsm.Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, + fsm.StateMachine[string, string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, - fsm.Callbacks{}, + fsm.Callbacks[string, string]{}, ) fmt.Println(fsm.Current()) diff --git a/examples/struct.go b/examples/struct.go index 17fa712..e355562 100644 --- a/examples/struct.go +++ b/examples/struct.go @@ -1,15 +1,17 @@ +//go:build ignore // +build ignore package main import ( "fmt" + "github.com/looplab/fsm" ) type Door struct { To string - FSM *fsm.FSM + FSM *fsm.FSM[string, string] } func NewDoor(to string) *Door { @@ -19,19 +21,19 @@ func NewDoor(to string) *Door { d.FSM = fsm.NewFSM( "closed", - fsm.Events{ - {Name: "open", Src: []string{"closed"}, Dst: "open"}, - {Name: "close", Src: []string{"open"}, Dst: "closed"}, + fsm.StateMachine[string, string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, - fsm.Callbacks{ - "enter_state": func(e *fsm.Event) { d.enterState(e) }, + fsm.Callbacks[string, string]{ + "enter_state": func(e *fsm.CallbackReference[string, string]) { d.enterState(e) }, }, ) return d } -func (d *Door) enterState(e *fsm.Event) { +func (d *Door) enterState(e *fsm.CallbackReference[string, string]) { fmt.Printf("The door to %s is %s\n", d.To, e.Dst) } diff --git a/fsm.go b/fsm.go index 2fa5620..e229533 100644 --- a/fsm.go +++ b/fsm.go @@ -447,7 +447,7 @@ const ( ) // cKey is a struct key used for keeping the callbacks mapped to a target. -type cKey[ES EventOrState] struct { +type cKey[ES EventOrState] struct { // FIXME Type // target is either the name of a state or an event depending on which // callback type the key refers to. It can also be "" for a non-targeted // callback like before_event. diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f3849cf --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/exp v0.0.0-20220706164943-b4a6d9510983 h1:sUweFwmLOje8KNfXAVqGGAsmgJ/F8jJ6wBLJDt4BTKY= +golang.org/x/exp v0.0.0-20220706164943-b4a6d9510983/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= From 114e2529faf556cbd71edf1c43ebc9aacdf0271e Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 12:47:28 +0200 Subject: [PATCH 05/33] any instead of interface{} --- .github/workflows/main.yml | 2 +- Makefile | 2 +- event.go | 2 +- fsm.go | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9967359..f126a41 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.16 + go-version: 1.18 - name: Test run: go test -coverprofile=coverage.out ./... diff --git a/Makefile b/Makefile index 821a539..214408a 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ default: services test .PHONY: test test: - go test -v -race ./... + CGO_ENABLED=1 go test ./... -v -race -coverprofile=coverage.out -covermode=atomic && go tool cover -func=coverage.out .PHONY: lint lint: diff --git a/event.go b/event.go index f761299..bcfd2cc 100644 --- a/event.go +++ b/event.go @@ -42,7 +42,7 @@ type CallbackReference[E Event, S State] struct { Err error // Args is an optional list of arguments passed to the callback. - Args []interface{} + Args []any // canceled is an internal flag set if the transition is canceled. canceled bool diff --git a/fsm.go b/fsm.go index e229533..e64d70c 100644 --- a/fsm.go +++ b/fsm.go @@ -59,7 +59,7 @@ type FSM[E Event, S State] struct { eventMu sync.Mutex // metadata can be used to store and load data that maybe used across events // use methods SetMetadata() and Metadata() to store and load data - metadata map[string]interface{} + metadata map[string]any metadataMu sync.RWMutex } @@ -134,7 +134,7 @@ func NewFSM[E Event, S State](initial S, events []StateMachineEntry[E, S], callb current: initial, transitions: make(map[eKey[E, S]]S), callbacks: make(map[cKey[E]]Callback[E, S]), - metadata: make(map[string]interface{}), + metadata: make(map[string]any), } // Build transition map and store sets of all events and states. @@ -255,7 +255,7 @@ func (f *FSM[E, S]) Cannot(event E) bool { } // Metadata returns the value stored in metadata -func (f *FSM[E, S]) Metadata(key string) (interface{}, bool) { +func (f *FSM[E, S]) Metadata(key string) (any, bool) { f.metadataMu.RLock() defer f.metadataMu.RUnlock() dataElement, ok := f.metadata[key] @@ -263,10 +263,10 @@ func (f *FSM[E, S]) Metadata(key string) (interface{}, bool) { } // SetMetadata stores the dataValue in metadata indexing it with key -func (f *FSM[E, S]) SetMetadata(key string, dataValue interface{}) { +func (f *FSM[E, S]) SetMetadata(key string, value any) { f.metadataMu.Lock() defer f.metadataMu.Unlock() - f.metadata[key] = dataValue + f.metadata[key] = value } // Event initiates a state transition with the named event. @@ -286,7 +286,7 @@ func (f *FSM[E, S]) SetMetadata(key string, dataValue interface{}) { // // The last error should never occur in this situation and is a sign of an // internal bug. -func (f *FSM[E, S]) Event(event E, args ...interface{}) error { +func (f *FSM[E, S]) Event(event E, args ...any) error { f.eventMu.Lock() defer f.eventMu.Unlock() From b3de70bbe283d827e277f560faff594a611deab5 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 12:50:30 +0200 Subject: [PATCH 06/33] gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f69a717..174d9f3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,6 @@ _testmain.go # Testing .coverprofile +coverage.out -.vscode \ No newline at end of file +.vscode From 698f85da0b99eeda12e332061cb015cd6c2188f8 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 13:11:42 +0200 Subject: [PATCH 07/33] Flows --- examples/alternate.go | 2 +- examples/data.go | 2 +- examples/simple.go | 2 +- examples/struct.go | 2 +- fsm.go | 10 ++--- fsm_test.go | 74 ++++++++++++++++++------------------- graphviz_visualizer_test.go | 2 +- mermaid_visualizer_test.go | 4 +- 8 files changed, 49 insertions(+), 49 deletions(-) diff --git a/examples/alternate.go b/examples/alternate.go index 9eef8ba..54289b0 100644 --- a/examples/alternate.go +++ b/examples/alternate.go @@ -13,7 +13,7 @@ func main() { fsm := fsm.NewFSM( "idle", - fsm.StateMachine[string, string]{ + fsm.Flows[string, string]{ {Event: "scan", Src: []string{"idle"}, Dst: "scanning"}, {Event: "working", Src: []string{"scanning"}, Dst: "scanning"}, {Event: "situation", Src: []string{"scanning"}, Dst: "scanning"}, diff --git a/examples/data.go b/examples/data.go index 3ef1c40..dddd2dd 100644 --- a/examples/data.go +++ b/examples/data.go @@ -12,7 +12,7 @@ import ( func main() { fsm := fsm.NewFSM( "idle", - fsm.StateMachine[string, string]{ + fsm.Flows[string, string]{ {Event: "produce", Src: []string{"idle"}, Dst: "idle"}, {Event: "consume", Src: []string{"idle"}, Dst: "idle"}, }, diff --git a/examples/simple.go b/examples/simple.go index 8e16c67..ac192bf 100644 --- a/examples/simple.go +++ b/examples/simple.go @@ -12,7 +12,7 @@ import ( func main() { fsm := fsm.NewFSM( "closed", - fsm.StateMachine[string, string]{ + fsm.Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, diff --git a/examples/struct.go b/examples/struct.go index e355562..4ec32c8 100644 --- a/examples/struct.go +++ b/examples/struct.go @@ -21,7 +21,7 @@ func NewDoor(to string) *Door { d.FSM = fsm.NewFSM( "closed", - fsm.StateMachine[string, string]{ + fsm.Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, diff --git a/fsm.go b/fsm.go index e64d70c..9690739 100644 --- a/fsm.go +++ b/fsm.go @@ -64,12 +64,12 @@ type FSM[E Event, S State] struct { metadataMu sync.RWMutex } -// StateMachineEntry represents an event when initializing the FSM. +// Flow represents an event when initializing the FSM. // // The event can have one or more source states that is valid for performing // the transition. If the FSM is in one of the source states it will end up in // the specified destination state, calling all defined callbacks as it goes. -type StateMachineEntry[E Event, S State] struct { +type Flow[E Event, S State] struct { // Event is the event name used when calling for a transition. Event E @@ -86,8 +86,8 @@ type StateMachineEntry[E Event, S State] struct { // event info as the callback happens. type Callback[E Event, S State] func(*CallbackReference[E, S]) -// StateMachine is a shorthand for defining the transition map in NewFSM. -type StateMachine[E Event, S State] []StateMachineEntry[E, S] +// Flows is a shorthand for defining the transition map in NewFSM. +type Flows[E Event, S State] []Flow[E, S] // Callbacks is a shorthand for defining the callbacks in NewFSM. type Callbacks[E Event, S State] map[string]Callback[E, S] @@ -128,7 +128,7 @@ type Callbacks[E Event, S State] map[string]Callback[E, S] // which version of the callback will end up in the internal map. This is due // to the pseudo random nature of Go maps. No checking for multiple keys is // currently performed. -func NewFSM[E Event, S State](initial S, events []StateMachineEntry[E, S], callbacks map[string]Callback[E, S]) *FSM[E, S] { +func NewFSM[E Event, S State](initial S, events []Flow[E, S], callbacks map[string]Callback[E, S]) *FSM[E, S] { f := &FSM[E, S]{ transitionerObj: &transitionerStruct[E, S]{}, current: initial, diff --git a/fsm_test.go b/fsm_test.go index bcdd256..976eab4 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -32,7 +32,7 @@ func (t fakeTransitionerObj[E, S]) transition(f *FSM[E, S]) error { func TestSameState(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string, string]{}, @@ -46,7 +46,7 @@ func TestSameState(t *testing.T) { func TestSetState(t *testing.T) { fsm := NewFSM( "walking", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "walk", Src: []string{"start"}, Dst: "walking"}, }, Callbacks[string, string]{}, @@ -64,7 +64,7 @@ func TestSetState(t *testing.T) { func TestBadTransition(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "running"}, }, Callbacks[string, string]{}, @@ -79,7 +79,7 @@ func TestBadTransition(t *testing.T) { func TestInappropriateEvent(t *testing.T) { fsm := NewFSM( "closed", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -94,7 +94,7 @@ func TestInappropriateEvent(t *testing.T) { func TestInvalidEvent(t *testing.T) { fsm := NewFSM( "closed", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -109,7 +109,7 @@ func TestInvalidEvent(t *testing.T) { func TestMultipleSources(t *testing.T) { fsm := NewFSM( "one", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "first", Src: []string{"one"}, Dst: "two"}, {Event: "second", Src: []string{"two"}, Dst: "three"}, {Event: "reset", Src: []string{"one", "two", "three"}, Dst: "one"}, @@ -154,7 +154,7 @@ func TestMultipleSources(t *testing.T) { func TestMultipleEvents(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "first", Src: []string{"start"}, Dst: "one"}, {Event: "second", Src: []string{"start"}, Dst: "two"}, {Event: "reset", Src: []string{"one"}, Dst: "reset_one"}, @@ -211,7 +211,7 @@ func TestGenericCallbacks(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -247,7 +247,7 @@ func TestSpecificCallbacks(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -281,7 +281,7 @@ func TestSpecificCallbacksShortform(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -308,7 +308,7 @@ func TestBeforeEventWithoutTransition(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "dontrun", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string, string]{ @@ -334,7 +334,7 @@ func TestBeforeEventWithoutTransition(t *testing.T) { func TestCancelBeforeGenericEvent(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -352,7 +352,7 @@ func TestCancelBeforeGenericEvent(t *testing.T) { func TestCancelBeforeSpecificEvent(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -370,7 +370,7 @@ func TestCancelBeforeSpecificEvent(t *testing.T) { func TestCancelLeaveGenericState(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -388,7 +388,7 @@ func TestCancelLeaveGenericState(t *testing.T) { func TestCancelLeaveSpecificState(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -406,7 +406,7 @@ func TestCancelLeaveSpecificState(t *testing.T) { func TestCancelWithError(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -432,7 +432,7 @@ func TestCancelWithError(t *testing.T) { func TestAsyncTransitionGenericState(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -457,7 +457,7 @@ func TestAsyncTransitionGenericState(t *testing.T) { func TestAsyncTransitionSpecificState(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -482,7 +482,7 @@ func TestAsyncTransitionSpecificState(t *testing.T) { func TestAsyncTransitionInProgress(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, {Event: "reset", Src: []string{"end"}, Dst: "start"}, }, @@ -513,7 +513,7 @@ func TestAsyncTransitionInProgress(t *testing.T) { func TestAsyncTransitionNotInProgress(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, {Event: "reset", Src: []string{"end"}, Dst: "start"}, }, @@ -528,7 +528,7 @@ func TestAsyncTransitionNotInProgress(t *testing.T) { func TestCallbackNoError(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -545,7 +545,7 @@ func TestCallbackNoError(t *testing.T) { func TestCallbackError(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -563,7 +563,7 @@ func TestCallbackError(t *testing.T) { func TestCallbackArgs(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -597,7 +597,7 @@ func TestCallbackPanic(t *testing.T) { }() fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -616,7 +616,7 @@ func TestNoDeadLock(t *testing.T) { var fsm *FSM[string, string] fsm = NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -634,7 +634,7 @@ func TestNoDeadLock(t *testing.T) { func TestThreadSafetyRaceCondition(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -661,7 +661,7 @@ func TestDoubleTransition(t *testing.T) { wg.Add(2) fsm = NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -697,7 +697,7 @@ func TestDoubleTransition(t *testing.T) { func TestNoTransition(t *testing.T) { fsm := NewFSM( "start", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string, string]{}, @@ -711,7 +711,7 @@ func TestNoTransition(t *testing.T) { func ExampleNewFSM() { fsm := NewFSM( "green", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "warn", Src: []string{"green"}, Dst: "yellow"}, {Event: "panic", Src: []string{"yellow"}, Dst: "red"}, {Event: "panic", Src: []string{"green"}, Dst: "red"}, @@ -767,7 +767,7 @@ func ExampleNewFSM() { func ExampleFSM_Current() { fsm := NewFSM( "closed", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -780,7 +780,7 @@ func ExampleFSM_Current() { func ExampleFSM_Is() { fsm := NewFSM( "closed", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -796,7 +796,7 @@ func ExampleFSM_Is() { func ExampleFSM_Can() { fsm := NewFSM( "closed", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -812,7 +812,7 @@ func ExampleFSM_Can() { func ExampleFSM_AvailableTransitions() { fsm := NewFSM( "closed", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, {Event: "kick", Src: []string{"closed"}, Dst: "broken"}, @@ -830,7 +830,7 @@ func ExampleFSM_AvailableTransitions() { func ExampleFSM_Cannot() { fsm := NewFSM( "closed", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -846,7 +846,7 @@ func ExampleFSM_Cannot() { func ExampleFSM_Event() { fsm := NewFSM( "closed", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -872,7 +872,7 @@ func ExampleFSM_Event() { func ExampleFSM_Transition() { fsm := NewFSM( "closed", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -911,7 +911,7 @@ const ( func ExampleFSM_Event_Generic() { fsm := NewFSM( IsClosed, - StateMachine[MyEvent, MyState]{ + Flows[MyEvent, MyState]{ {Event: Open, Src: []MyState{IsClosed}, Dst: IsOpen}, {Event: Close, Src: []MyState{IsOpen}, Dst: IsClosed}, }, diff --git a/graphviz_visualizer_test.go b/graphviz_visualizer_test.go index 6df6e7f..3befcdf 100644 --- a/graphviz_visualizer_test.go +++ b/graphviz_visualizer_test.go @@ -9,7 +9,7 @@ import ( func TestGraphvizOutput(t *testing.T) { fsmUnderTest := NewFSM( "closed", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, {Event: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, diff --git a/mermaid_visualizer_test.go b/mermaid_visualizer_test.go index b17cc29..30c7e5e 100644 --- a/mermaid_visualizer_test.go +++ b/mermaid_visualizer_test.go @@ -9,7 +9,7 @@ import ( func TestMermaidOutput(t *testing.T) { fsmUnderTest := NewFSM( "closed", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, {Event: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, @@ -40,7 +40,7 @@ stateDiagram-v2 func TestMermaidFlowChartOutput(t *testing.T) { fsmUnderTest := NewFSM( "closed", - StateMachine[string, string]{ + Flows[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "part-open", Src: []string{"closed"}, Dst: "intermediate"}, {Event: "part-open", Src: []string{"intermediate"}, Dst: "open"}, From 4be1559da0f4009af3c93ea2e295028d86d294f3 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 13:19:45 +0200 Subject: [PATCH 08/33] Naming --- event.go | 8 ++--- examples/alternate.go | 8 ++--- examples/data.go | 4 +-- examples/struct.go | 4 +-- fsm.go | 18 +++++----- fsm_test.go | 76 +++++++++++++++++++++++-------------------- 6 files changed, 61 insertions(+), 57 deletions(-) diff --git a/event.go b/event.go index bcfd2cc..c4d8c4e 100644 --- a/event.go +++ b/event.go @@ -24,8 +24,8 @@ type EventOrState interface { Event | State } -// CallbackReference is the info that get passed as a reference in the callbacks. -type CallbackReference[E Event, S State] struct { +// CallbackContext is the info that get passed as a reference in the callbacks. +type CallbackContext[E Event, S State] struct { // FSM is an reference to the current FSM. FSM *FSM[E, S] @@ -54,7 +54,7 @@ type CallbackReference[E Event, S State] struct { // Cancel can be called in before_ or leave_ to cancel the // current transition before it happens. It takes an optional error, which will // overwrite e.Err if set before. -func (e *CallbackReference[E, S]) Cancel(err ...error) { +func (e *CallbackContext[E, S]) Cancel(err ...error) { e.canceled = true if len(err) > 0 { @@ -67,6 +67,6 @@ func (e *CallbackReference[E, S]) Cancel(err ...error) { // The current state transition will be on hold in the old state until a final // call to Transition is made. This will complete the transition and possibly // call the other callbacks. -func (e *CallbackReference[E, S]) Async() { +func (e *CallbackContext[E, S]) Async() { e.async = true } diff --git a/examples/alternate.go b/examples/alternate.go index 54289b0..e9c04a0 100644 --- a/examples/alternate.go +++ b/examples/alternate.go @@ -21,16 +21,16 @@ func main() { {Event: "finish", Src: []string{"scanning"}, Dst: "idle"}, }, fsm.Callbacks[string, string]{ - "scan": func(e *fsm.CallbackReference[string, string]) { + "scan": func(e *fsm.CallbackContext[string, string]) { fmt.Println("after_scan: " + e.FSM.Current()) }, - "working": func(e *fsm.CallbackReference[string, string]) { + "working": func(e *fsm.CallbackContext[string, string]) { fmt.Println("working: " + e.FSM.Current()) }, - "situation": func(e *fsm.CallbackReference[string, string]) { + "situation": func(e *fsm.CallbackContext[string, string]) { fmt.Println("situation: " + e.FSM.Current()) }, - "finish": func(e *fsm.CallbackReference[string, string]) { + "finish": func(e *fsm.CallbackContext[string, string]) { fmt.Println("finish: " + e.FSM.Current()) }, }, diff --git a/examples/data.go b/examples/data.go index dddd2dd..6da3a66 100644 --- a/examples/data.go +++ b/examples/data.go @@ -17,11 +17,11 @@ func main() { {Event: "consume", Src: []string{"idle"}, Dst: "idle"}, }, fsm.Callbacks[string, string]{ - "produce": func(e *fsm.CallbackReference[string, string]) { + "produce": func(e *fsm.CallbackContext[string, string]) { e.FSM.SetMetadata("message", "hii") fmt.Println("produced data") }, - "consume": func(e *fsm.CallbackReference[string, string]) { + "consume": func(e *fsm.CallbackContext[string, string]) { message, ok := e.FSM.Metadata("message") if ok { fmt.Println("message = " + message.(string)) diff --git a/examples/struct.go b/examples/struct.go index 4ec32c8..fc27a29 100644 --- a/examples/struct.go +++ b/examples/struct.go @@ -26,14 +26,14 @@ func NewDoor(to string) *Door { {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, fsm.Callbacks[string, string]{ - "enter_state": func(e *fsm.CallbackReference[string, string]) { d.enterState(e) }, + "enter_state": func(e *fsm.CallbackContext[string, string]) { d.enterState(e) }, }, ) return d } -func (d *Door) enterState(e *fsm.CallbackReference[string, string]) { +func (d *Door) enterState(e *fsm.CallbackContext[string, string]) { fmt.Printf("The door to %s is %s\n", d.To, e.Dst) } diff --git a/fsm.go b/fsm.go index 9690739..06893da 100644 --- a/fsm.go +++ b/fsm.go @@ -84,7 +84,7 @@ type Flow[E Event, S State] struct { // Callback is a function type that callbacks should use. Event is the current // event info as the callback happens. -type Callback[E Event, S State] func(*CallbackReference[E, S]) +type Callback[E Event, S State] func(*CallbackContext[E, S]) // Flows is a shorthand for defining the transition map in NewFSM. type Flows[E Event, S State] []Flow[E, S] @@ -236,13 +236,13 @@ func (f *FSM[E, S]) Can(event E) bool { // AvailableTransitions returns a list of transitions available in the // current state. -func (f *FSM[E, S]) AvailableTransitions() []string { +func (f *FSM[E, S]) AvailableTransitions() []E { f.stateMu.RLock() defer f.stateMu.RUnlock() - var transitions []string + var transitions []E for key := range f.transitions { if key.src == f.current { - transitions = append(transitions, string(key.event)) + transitions = append(transitions, key.event) } } return transitions @@ -307,7 +307,7 @@ func (f *FSM[E, S]) Event(event E, args ...any) error { return UnknownEventError{string(event)} } - e := &CallbackReference[E, S]{f, event, f.current, dst, nil, args, false, false} + e := &CallbackContext[E, S]{f, event, f.current, dst, nil, args, false, false} err := f.beforeEventCallbacks(e) if err != nil { @@ -378,7 +378,7 @@ func (t transitionerStruct[E, S]) transition(f *FSM[E, S]) error { // beforeEventCallbacks calls the before_ callbacks, first the named then the // general version. -func (f *FSM[E, S]) beforeEventCallbacks(e *CallbackReference[E, S]) error { +func (f *FSM[E, S]) beforeEventCallbacks(e *CallbackContext[E, S]) error { if fn, ok := f.callbacks[cKey[E]{e.Event, callbackBeforeEvent}]; ok { fn(e) if e.canceled { @@ -396,7 +396,7 @@ func (f *FSM[E, S]) beforeEventCallbacks(e *CallbackReference[E, S]) error { // leaveStateCallbacks calls the leave_ callbacks, first the named then the // general version. -func (f *FSM[E, S]) leaveStateCallbacks(e *CallbackReference[E, S]) error { +func (f *FSM[E, S]) leaveStateCallbacks(e *CallbackContext[E, S]) error { if fn, ok := f.callbacks[cKey[E]{E(f.current), callbackLeaveState}]; ok { // FIXME fn(e) if e.canceled { @@ -418,7 +418,7 @@ func (f *FSM[E, S]) leaveStateCallbacks(e *CallbackReference[E, S]) error { // enterStateCallbacks calls the enter_ callbacks, first the named then the // general version. -func (f *FSM[E, S]) enterStateCallbacks(e *CallbackReference[E, S]) { +func (f *FSM[E, S]) enterStateCallbacks(e *CallbackContext[E, S]) { if fn, ok := f.callbacks[cKey[E]{E(f.current), callbackEnterState}]; ok { // FIXME fn(e) } @@ -429,7 +429,7 @@ func (f *FSM[E, S]) enterStateCallbacks(e *CallbackReference[E, S]) { // afterEventCallbacks calls the after_ callbacks, first the named then the // general version. -func (f *FSM[E, S]) afterEventCallbacks(e *CallbackReference[E, S]) { +func (f *FSM[E, S]) afterEventCallbacks(e *CallbackContext[E, S]) { if fn, ok := f.callbacks[cKey[E]{e.Event, callbackAfterEvent}]; ok { fn(e) } diff --git a/fsm_test.go b/fsm_test.go index 976eab4..dde4e7c 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -215,16 +215,16 @@ func TestGenericCallbacks(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "before_event": func(e *CallbackReference[string, string]) { + "before_event": func(e *CallbackContext[string, string]) { beforeEvent = true }, - "leave_state": func(e *CallbackReference[string, string]) { + "leave_state": func(e *CallbackContext[string, string]) { leaveState = true }, - "enter_state": func(e *CallbackReference[string, string]) { + "enter_state": func(e *CallbackContext[string, string]) { enterState = true }, - "after_event": func(e *CallbackReference[string, string]) { + "after_event": func(e *CallbackContext[string, string]) { afterEvent = true }, }, @@ -251,16 +251,16 @@ func TestSpecificCallbacks(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "before_run": func(e *CallbackReference[string, string]) { + "before_run": func(e *CallbackContext[string, string]) { beforeEvent = true }, - "leave_start": func(e *CallbackReference[string, string]) { + "leave_start": func(e *CallbackContext[string, string]) { leaveState = true }, - "enter_end": func(e *CallbackReference[string, string]) { + "enter_end": func(e *CallbackContext[string, string]) { enterState = true }, - "after_run": func(e *CallbackReference[string, string]) { + "after_run": func(e *CallbackContext[string, string]) { afterEvent = true }, }, @@ -285,10 +285,10 @@ func TestSpecificCallbacksShortform(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "end": func(e *CallbackReference[string, string]) { + "end": func(e *CallbackContext[string, string]) { enterState = true }, - "run": func(e *CallbackReference[string, string]) { + "run": func(e *CallbackContext[string, string]) { afterEvent = true }, }, @@ -312,7 +312,7 @@ func TestBeforeEventWithoutTransition(t *testing.T) { {Event: "dontrun", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string, string]{ - "before_event": func(e *CallbackReference[string, string]) { + "before_event": func(e *CallbackContext[string, string]) { beforeEvent = true }, }, @@ -338,7 +338,7 @@ func TestCancelBeforeGenericEvent(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "before_event": func(e *CallbackReference[string, string]) { + "before_event": func(e *CallbackContext[string, string]) { e.Cancel() }, }, @@ -356,7 +356,7 @@ func TestCancelBeforeSpecificEvent(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "before_run": func(e *CallbackReference[string, string]) { + "before_run": func(e *CallbackContext[string, string]) { e.Cancel() }, }, @@ -374,7 +374,7 @@ func TestCancelLeaveGenericState(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "leave_state": func(e *CallbackReference[string, string]) { + "leave_state": func(e *CallbackContext[string, string]) { e.Cancel() }, }, @@ -392,7 +392,7 @@ func TestCancelLeaveSpecificState(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "leave_start": func(e *CallbackReference[string, string]) { + "leave_start": func(e *CallbackContext[string, string]) { e.Cancel() }, }, @@ -410,7 +410,7 @@ func TestCancelWithError(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "before_event": func(e *CallbackReference[string, string]) { + "before_event": func(e *CallbackContext[string, string]) { e.Cancel(fmt.Errorf("error")) }, }, @@ -436,7 +436,7 @@ func TestAsyncTransitionGenericState(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "leave_state": func(e *CallbackReference[string, string]) { + "leave_state": func(e *CallbackContext[string, string]) { e.Async() }, }, @@ -461,7 +461,7 @@ func TestAsyncTransitionSpecificState(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "leave_start": func(e *CallbackReference[string, string]) { + "leave_start": func(e *CallbackContext[string, string]) { e.Async() }, }, @@ -487,7 +487,7 @@ func TestAsyncTransitionInProgress(t *testing.T) { {Event: "reset", Src: []string{"end"}, Dst: "start"}, }, Callbacks[string, string]{ - "leave_start": func(e *CallbackReference[string, string]) { + "leave_start": func(e *CallbackContext[string, string]) { e.Async() }, }, @@ -532,7 +532,7 @@ func TestCallbackNoError(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "run": func(e *CallbackReference[string, string]) { + "run": func(e *CallbackContext[string, string]) { }, }, ) @@ -549,7 +549,7 @@ func TestCallbackError(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "run": func(e *CallbackReference[string, string]) { + "run": func(e *CallbackContext[string, string]) { e.Err = fmt.Errorf("error") }, }, @@ -567,7 +567,7 @@ func TestCallbackArgs(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "run": func(e *CallbackReference[string, string]) { + "run": func(e *CallbackContext[string, string]) { if len(e.Args) != 1 { t.Error("too few arguments") } @@ -601,7 +601,7 @@ func TestCallbackPanic(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "run": func(e *CallbackReference[string, string]) { + "run": func(e *CallbackContext[string, string]) { panic(panicMsg) }, }, @@ -620,7 +620,7 @@ func TestNoDeadLock(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "run": func(e *CallbackReference[string, string]) { + "run": func(e *CallbackContext[string, string]) { fsm.Current() // Should not result in a panic / deadlock }, }, @@ -638,7 +638,7 @@ func TestThreadSafetyRaceCondition(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "run": func(e *CallbackReference[string, string]) { + "run": func(e *CallbackContext[string, string]) { }, }, ) @@ -665,7 +665,7 @@ func TestDoubleTransition(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "before_run": func(e *CallbackReference[string, string]) { + "before_run": func(e *CallbackContext[string, string]) { wg.Done() // Imagine a concurrent event coming in of the same type while // the data access mutex is unlocked because the current transition @@ -719,28 +719,28 @@ func ExampleNewFSM() { {Event: "clear", Src: []string{"yellow"}, Dst: "green"}, }, Callbacks[string, string]{ - "before_warn": func(e *CallbackReference[string, string]) { + "before_warn": func(e *CallbackContext[string, string]) { fmt.Println("before_warn") }, - "before_event": func(e *CallbackReference[string, string]) { + "before_event": func(e *CallbackContext[string, string]) { fmt.Println("before_event") }, - "leave_green": func(e *CallbackReference[string, string]) { + "leave_green": func(e *CallbackContext[string, string]) { fmt.Println("leave_green") }, - "leave_state": func(e *CallbackReference[string, string]) { + "leave_state": func(e *CallbackContext[string, string]) { fmt.Println("leave_state") }, - "enter_yellow": func(e *CallbackReference[string, string]) { + "enter_yellow": func(e *CallbackContext[string, string]) { fmt.Println("enter_yellow") }, - "enter_state": func(e *CallbackReference[string, string]) { + "enter_state": func(e *CallbackContext[string, string]) { fmt.Println("enter_state") }, - "after_warn": func(e *CallbackReference[string, string]) { + "after_warn": func(e *CallbackContext[string, string]) { fmt.Println("after_warn") }, - "after_event": func(e *CallbackReference[string, string]) { + "after_event": func(e *CallbackContext[string, string]) { fmt.Println("after_event") }, }, @@ -877,7 +877,7 @@ func ExampleFSM_Transition() { {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, Callbacks[string, string]{ - "leave_closed": func(e *CallbackReference[string, string]) { + "leave_closed": func(e *CallbackContext[string, string]) { e.Async() }, }, @@ -915,7 +915,11 @@ func ExampleFSM_Event_Generic() { {Event: Open, Src: []MyState{IsClosed}, Dst: IsOpen}, {Event: Close, Src: []MyState{IsOpen}, Dst: IsClosed}, }, - Callbacks[MyEvent, MyState]{}, + Callbacks[MyEvent, MyState]{ + "": func(cr *CallbackContext[MyEvent, MyState]) { + + }, + }, ) fmt.Println(fsm.Current()) err := fsm.Event(Open) From 4e23d6ff684f130ca1af1de5957300aad6bf1e4c Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 13:28:15 +0200 Subject: [PATCH 09/33] One more --- fsm.go | 5 +++-- fsm_test.go | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/fsm.go b/fsm.go index 06893da..fd80c74 100644 --- a/fsm.go +++ b/fsm.go @@ -90,7 +90,7 @@ type Callback[E Event, S State] func(*CallbackContext[E, S]) type Flows[E Event, S State] []Flow[E, S] // Callbacks is a shorthand for defining the callbacks in NewFSM. -type Callbacks[E Event, S State] map[string]Callback[E, S] +type Callbacks[E Event, S State] map[E]Callback[E, S] // NewFSM constructs a FSM from events and callbacks. // @@ -128,7 +128,7 @@ type Callbacks[E Event, S State] map[string]Callback[E, S] // which version of the callback will end up in the internal map. This is due // to the pseudo random nature of Go maps. No checking for multiple keys is // currently performed. -func NewFSM[E Event, S State](initial S, events []Flow[E, S], callbacks map[string]Callback[E, S]) *FSM[E, S] { +func NewFSM[E Event, S State](initial S, events []Flow[E, S], callbacks map[E]Callback[E, S]) *FSM[E, S] { f := &FSM[E, S]{ transitionerObj: &transitionerStruct[E, S]{}, current: initial, @@ -154,6 +154,7 @@ func NewFSM[E Event, S State](initial S, events []Flow[E, S], callbacks map[stri var target string var callbackType int + name := string(name) // FIXME switch { case strings.HasPrefix(name, "before_"): target = strings.TrimPrefix(name, "before_") diff --git a/fsm_test.go b/fsm_test.go index dde4e7c..6260a4b 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -903,6 +903,7 @@ type MyState string const ( Close MyEvent = "close" Open MyEvent = "open" + Any MyEvent = "" IsClosed MyState = "closed" IsOpen MyState = "open" @@ -916,7 +917,7 @@ func ExampleFSM_Event_Generic() { {Event: Close, Src: []MyState{IsOpen}, Dst: IsClosed}, }, Callbacks[MyEvent, MyState]{ - "": func(cr *CallbackContext[MyEvent, MyState]) { + Any: func(cr *CallbackContext[MyEvent, MyState]) { }, }, From 459416db08b9be57916e18f65a2c3561f9ec9e0b Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 13:28:42 +0200 Subject: [PATCH 10/33] One more --- examples/generic.go | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 examples/generic.go diff --git a/examples/generic.go b/examples/generic.go new file mode 100644 index 0000000..d185282 --- /dev/null +++ b/examples/generic.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + + "github.com/looplab/fsm" +) + +type MyEvent string +type MyState string + +const ( + Close MyEvent = "close" + Open MyEvent = "open" + Any MyEvent = "" + + IsClosed MyState = "closed" + IsOpen MyState = "open" +) + +func main() { + fsm := fsm.NewFSM( + IsClosed, + fsm.Flows[MyEvent, MyState]{ + {Event: Open, Src: []MyState{IsClosed}, Dst: IsOpen}, + {Event: Close, Src: []MyState{IsOpen}, Dst: IsClosed}, + }, + fsm.Callbacks[MyEvent, MyState]{ + Open: func(cr *fsm.CallbackContext[MyEvent, MyState]) { + fmt.Printf("callback: event:%s src:%s dst:%s\n", cr.Event, cr.Src, cr.Dst) + }, + Any: func(cr *fsm.CallbackContext[MyEvent, MyState]) { + fmt.Printf("callback: event:%s src:%s dst:%s\n", cr.Event, cr.Src, cr.Dst) + }, + }, + ) + fmt.Println(fsm.Current()) + err := fsm.Event(Open) + if err != nil { + fmt.Println(err) + } + fmt.Println(fsm.Current()) + err = fsm.Event(Close) + if err != nil { + fmt.Println(err) + } + fmt.Println(fsm.Current()) + // Output: + // closed + // open + // closed +} From 80bc866d7aa86530b85e8dfb7375329e4ecfe42e Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 14:26:53 +0200 Subject: [PATCH 11/33] renaming again --- README.md | 4 +- examples/alternate.go | 4 +- examples/data.go | 4 +- examples/generic.go | 4 +- examples/simple.go | 4 +- examples/struct.go | 4 +- fsm.go | 36 ++++----- fsm_test.go | 148 ++++++++++++++++++------------------ graphviz_visualizer_test.go | 4 +- mermaid_visualizer_test.go | 8 +- 10 files changed, 110 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 57c5511..f11b126 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ import ( ) func main() { - fsm := fsm.NewFSM( + fsm := fsm.New( "closed", fsm.Events{ {Name: "open", Src: []string{"closed"}, Dst: "open"}, @@ -77,7 +77,7 @@ func NewDoor(to string) *Door { To: to, } - d.FSM = fsm.NewFSM( + d.FSM = fsm.New( "closed", fsm.Events{ {Name: "open", Src: []string{"closed"}, Dst: "open"}, diff --git a/examples/alternate.go b/examples/alternate.go index e9c04a0..0fd4ee1 100644 --- a/examples/alternate.go +++ b/examples/alternate.go @@ -11,9 +11,9 @@ import ( func main() { - fsm := fsm.NewFSM( + fsm := fsm.New( "idle", - fsm.Flows[string, string]{ + fsm.Transistions[string, string]{ {Event: "scan", Src: []string{"idle"}, Dst: "scanning"}, {Event: "working", Src: []string{"scanning"}, Dst: "scanning"}, {Event: "situation", Src: []string{"scanning"}, Dst: "scanning"}, diff --git a/examples/data.go b/examples/data.go index 6da3a66..a62ad3f 100644 --- a/examples/data.go +++ b/examples/data.go @@ -10,9 +10,9 @@ import ( ) func main() { - fsm := fsm.NewFSM( + fsm := fsm.New( "idle", - fsm.Flows[string, string]{ + fsm.Transistions[string, string]{ {Event: "produce", Src: []string{"idle"}, Dst: "idle"}, {Event: "consume", Src: []string{"idle"}, Dst: "idle"}, }, diff --git a/examples/generic.go b/examples/generic.go index d185282..5f0c80d 100644 --- a/examples/generic.go +++ b/examples/generic.go @@ -19,9 +19,9 @@ const ( ) func main() { - fsm := fsm.NewFSM( + fsm := fsm.New( IsClosed, - fsm.Flows[MyEvent, MyState]{ + fsm.Transitions[MyEvent, MyState]{ {Event: Open, Src: []MyState{IsClosed}, Dst: IsOpen}, {Event: Close, Src: []MyState{IsOpen}, Dst: IsClosed}, }, diff --git a/examples/simple.go b/examples/simple.go index ac192bf..a587b09 100644 --- a/examples/simple.go +++ b/examples/simple.go @@ -10,9 +10,9 @@ import ( ) func main() { - fsm := fsm.NewFSM( + fsm := fsm.New( "closed", - fsm.Flows[string, string]{ + fsm.Transistions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, diff --git a/examples/struct.go b/examples/struct.go index fc27a29..ed2e168 100644 --- a/examples/struct.go +++ b/examples/struct.go @@ -19,9 +19,9 @@ func NewDoor(to string) *Door { To: to, } - d.FSM = fsm.NewFSM( + d.FSM = fsm.New( "closed", - fsm.Flows[string, string]{ + fsm.Transistions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, diff --git a/fsm.go b/fsm.go index fd80c74..d95463c 100644 --- a/fsm.go +++ b/fsm.go @@ -64,12 +64,12 @@ type FSM[E Event, S State] struct { metadataMu sync.RWMutex } -// Flow represents an event when initializing the FSM. +// Transition represents an event when initializing the FSM. // // The event can have one or more source states that is valid for performing // the transition. If the FSM is in one of the source states it will end up in // the specified destination state, calling all defined callbacks as it goes. -type Flow[E Event, S State] struct { +type Transition[E Event, S State] struct { // Event is the event name used when calling for a transition. Event E @@ -86,13 +86,13 @@ type Flow[E Event, S State] struct { // event info as the callback happens. type Callback[E Event, S State] func(*CallbackContext[E, S]) -// Flows is a shorthand for defining the transition map in NewFSM. -type Flows[E Event, S State] []Flow[E, S] +// Transitions is a shorthand for defining the transition map in NewFSM. +type Transitions[E Event, S State] []Transition[E, S] // Callbacks is a shorthand for defining the callbacks in NewFSM. type Callbacks[E Event, S State] map[E]Callback[E, S] -// NewFSM constructs a FSM from events and callbacks. +// New constructs a FSM from events and callbacks. // // The events and transitions are specified as a slice of Event structs // specified as Events. Each Event is mapped to one or more internal @@ -128,7 +128,7 @@ type Callbacks[E Event, S State] map[E]Callback[E, S] // which version of the callback will end up in the internal map. This is due // to the pseudo random nature of Go maps. No checking for multiple keys is // currently performed. -func NewFSM[E Event, S State](initial S, events []Flow[E, S], callbacks map[E]Callback[E, S]) *FSM[E, S] { +func New[E Event, S State](initial S, transitions []Transition[E, S], callbacks map[E]Callback[E, S]) *FSM[E, S] { f := &FSM[E, S]{ transitionerObj: &transitionerStruct[E, S]{}, current: initial, @@ -140,7 +140,7 @@ func NewFSM[E Event, S State](initial S, events []Flow[E, S], callbacks map[E]Ca // Build transition map and store sets of all events and states. allEvents := make(map[E]bool) allStates := make(map[S]bool) - for _, e := range events { + for _, e := range transitions { for _, src := range e.Src { f.transitions[eKey[E, S]{e.Event, src}] = e.Dst allStates[src] = true @@ -150,38 +150,38 @@ func NewFSM[E Event, S State](initial S, events []Flow[E, S], callbacks map[E]Ca } // Map all callbacks to events/states. - for name, fn := range callbacks { + for event, fn := range callbacks { var target string var callbackType int - name := string(name) // FIXME + eventName := string(event) // FIXME switch { - case strings.HasPrefix(name, "before_"): - target = strings.TrimPrefix(name, "before_") + case strings.HasPrefix(eventName, "before_"): + target = strings.TrimPrefix(eventName, "before_") if target == "event" { target = "" callbackType = callbackBeforeEvent } else if _, ok := allEvents[E(target)]; ok { // FIXME callbackType = callbackBeforeEvent } - case strings.HasPrefix(name, "leave_"): - target = strings.TrimPrefix(name, "leave_") + case strings.HasPrefix(eventName, "leave_"): + target = strings.TrimPrefix(eventName, "leave_") if target == "state" { target = "" callbackType = callbackLeaveState } else if _, ok := allStates[S(target)]; ok { callbackType = callbackLeaveState } - case strings.HasPrefix(name, "enter_"): - target = strings.TrimPrefix(name, "enter_") + case strings.HasPrefix(eventName, "enter_"): + target = strings.TrimPrefix(eventName, "enter_") if target == "state" { target = "" callbackType = callbackEnterState } else if _, ok := allStates[S(target)]; ok { callbackType = callbackEnterState } - case strings.HasPrefix(name, "after_"): - target = strings.TrimPrefix(name, "after_") + case strings.HasPrefix(eventName, "after_"): + target = strings.TrimPrefix(eventName, "after_") if target == "event" { target = "" callbackType = callbackAfterEvent @@ -189,7 +189,7 @@ func NewFSM[E Event, S State](initial S, events []Flow[E, S], callbacks map[E]Ca callbackType = callbackAfterEvent } default: - target = name + target = eventName if _, ok := allStates[S(target)]; ok { callbackType = callbackEnterState } else if _, ok := allEvents[E(target)]; ok { // FIXME diff --git a/fsm_test.go b/fsm_test.go index 6260a4b..0307065 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -30,9 +30,9 @@ func (t fakeTransitionerObj[E, S]) transition(f *FSM[E, S]) error { } func TestSameState(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string, string]{}, @@ -44,9 +44,9 @@ func TestSameState(t *testing.T) { } func TestSetState(t *testing.T) { - fsm := NewFSM( + fsm := New( "walking", - Flows[string, string]{ + Transitions[string, string]{ {Event: "walk", Src: []string{"start"}, Dst: "walking"}, }, Callbacks[string, string]{}, @@ -62,9 +62,9 @@ func TestSetState(t *testing.T) { } func TestBadTransition(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "running"}, }, Callbacks[string, string]{}, @@ -77,9 +77,9 @@ func TestBadTransition(t *testing.T) { } func TestInappropriateEvent(t *testing.T) { - fsm := NewFSM( + fsm := New( "closed", - Flows[string, string]{ + Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -92,9 +92,9 @@ func TestInappropriateEvent(t *testing.T) { } func TestInvalidEvent(t *testing.T) { - fsm := NewFSM( + fsm := New( "closed", - Flows[string, string]{ + Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -107,9 +107,9 @@ func TestInvalidEvent(t *testing.T) { } func TestMultipleSources(t *testing.T) { - fsm := NewFSM( + fsm := New( "one", - Flows[string, string]{ + Transitions[string, string]{ {Event: "first", Src: []string{"one"}, Dst: "two"}, {Event: "second", Src: []string{"two"}, Dst: "three"}, {Event: "reset", Src: []string{"one", "two", "three"}, Dst: "one"}, @@ -152,9 +152,9 @@ func TestMultipleSources(t *testing.T) { } func TestMultipleEvents(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "first", Src: []string{"start"}, Dst: "one"}, {Event: "second", Src: []string{"start"}, Dst: "two"}, {Event: "reset", Src: []string{"one"}, Dst: "reset_one"}, @@ -209,9 +209,9 @@ func TestGenericCallbacks(t *testing.T) { enterState := false afterEvent := false - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -245,9 +245,9 @@ func TestSpecificCallbacks(t *testing.T) { enterState := false afterEvent := false - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -279,9 +279,9 @@ func TestSpecificCallbacksShortform(t *testing.T) { enterState := false afterEvent := false - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -306,9 +306,9 @@ func TestSpecificCallbacksShortform(t *testing.T) { func TestBeforeEventWithoutTransition(t *testing.T) { beforeEvent := true - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "dontrun", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string, string]{ @@ -332,9 +332,9 @@ func TestBeforeEventWithoutTransition(t *testing.T) { } func TestCancelBeforeGenericEvent(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -350,9 +350,9 @@ func TestCancelBeforeGenericEvent(t *testing.T) { } func TestCancelBeforeSpecificEvent(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -368,9 +368,9 @@ func TestCancelBeforeSpecificEvent(t *testing.T) { } func TestCancelLeaveGenericState(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -386,9 +386,9 @@ func TestCancelLeaveGenericState(t *testing.T) { } func TestCancelLeaveSpecificState(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -404,9 +404,9 @@ func TestCancelLeaveSpecificState(t *testing.T) { } func TestCancelWithError(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -430,9 +430,9 @@ func TestCancelWithError(t *testing.T) { } func TestAsyncTransitionGenericState(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -455,9 +455,9 @@ func TestAsyncTransitionGenericState(t *testing.T) { } func TestAsyncTransitionSpecificState(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -480,9 +480,9 @@ func TestAsyncTransitionSpecificState(t *testing.T) { } func TestAsyncTransitionInProgress(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, {Event: "reset", Src: []string{"end"}, Dst: "start"}, }, @@ -511,9 +511,9 @@ func TestAsyncTransitionInProgress(t *testing.T) { } func TestAsyncTransitionNotInProgress(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, {Event: "reset", Src: []string{"end"}, Dst: "start"}, }, @@ -526,9 +526,9 @@ func TestAsyncTransitionNotInProgress(t *testing.T) { } func TestCallbackNoError(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -543,9 +543,9 @@ func TestCallbackNoError(t *testing.T) { } func TestCallbackError(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -561,9 +561,9 @@ func TestCallbackError(t *testing.T) { } func TestCallbackArgs(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -595,9 +595,9 @@ func TestCallbackPanic(t *testing.T) { t.Errorf("expected panic message to be '%s', got %v", panicMsg, r) } }() - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -614,9 +614,9 @@ func TestCallbackPanic(t *testing.T) { func TestNoDeadLock(t *testing.T) { var fsm *FSM[string, string] - fsm = NewFSM( + fsm = New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -632,9 +632,9 @@ func TestNoDeadLock(t *testing.T) { } func TestThreadSafetyRaceCondition(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -659,9 +659,9 @@ func TestDoubleTransition(t *testing.T) { var fsm *FSM[string, string] var wg sync.WaitGroup wg.Add(2) - fsm = NewFSM( + fsm = New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ @@ -695,9 +695,9 @@ func TestDoubleTransition(t *testing.T) { } func TestNoTransition(t *testing.T) { - fsm := NewFSM( + fsm := New( "start", - Flows[string, string]{ + Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string, string]{}, @@ -709,9 +709,9 @@ func TestNoTransition(t *testing.T) { } func ExampleNewFSM() { - fsm := NewFSM( + fsm := New( "green", - Flows[string, string]{ + Transitions[string, string]{ {Event: "warn", Src: []string{"green"}, Dst: "yellow"}, {Event: "panic", Src: []string{"yellow"}, Dst: "red"}, {Event: "panic", Src: []string{"green"}, Dst: "red"}, @@ -765,9 +765,9 @@ func ExampleNewFSM() { } func ExampleFSM_Current() { - fsm := NewFSM( + fsm := New( "closed", - Flows[string, string]{ + Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -778,9 +778,9 @@ func ExampleFSM_Current() { } func ExampleFSM_Is() { - fsm := NewFSM( + fsm := New( "closed", - Flows[string, string]{ + Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -794,9 +794,9 @@ func ExampleFSM_Is() { } func ExampleFSM_Can() { - fsm := NewFSM( + fsm := New( "closed", - Flows[string, string]{ + Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -810,9 +810,9 @@ func ExampleFSM_Can() { } func ExampleFSM_AvailableTransitions() { - fsm := NewFSM( + fsm := New( "closed", - Flows[string, string]{ + Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, {Event: "kick", Src: []string{"closed"}, Dst: "broken"}, @@ -828,9 +828,9 @@ func ExampleFSM_AvailableTransitions() { } func ExampleFSM_Cannot() { - fsm := NewFSM( + fsm := New( "closed", - Flows[string, string]{ + Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -844,9 +844,9 @@ func ExampleFSM_Cannot() { } func ExampleFSM_Event() { - fsm := NewFSM( + fsm := New( "closed", - Flows[string, string]{ + Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -870,9 +870,9 @@ func ExampleFSM_Event() { } func ExampleFSM_Transition() { - fsm := NewFSM( + fsm := New( "closed", - Flows[string, string]{ + Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, @@ -910,9 +910,9 @@ const ( ) func ExampleFSM_Event_Generic() { - fsm := NewFSM( + fsm := New( IsClosed, - Flows[MyEvent, MyState]{ + Transitions[MyEvent, MyState]{ {Event: Open, Src: []MyState{IsClosed}, Dst: IsOpen}, {Event: Close, Src: []MyState{IsOpen}, Dst: IsClosed}, }, diff --git a/graphviz_visualizer_test.go b/graphviz_visualizer_test.go index 3befcdf..ccb3b70 100644 --- a/graphviz_visualizer_test.go +++ b/graphviz_visualizer_test.go @@ -7,9 +7,9 @@ import ( ) func TestGraphvizOutput(t *testing.T) { - fsmUnderTest := NewFSM( + fsmUnderTest := New( "closed", - Flows[string, string]{ + Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, {Event: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, diff --git a/mermaid_visualizer_test.go b/mermaid_visualizer_test.go index 30c7e5e..c93a439 100644 --- a/mermaid_visualizer_test.go +++ b/mermaid_visualizer_test.go @@ -7,9 +7,9 @@ import ( ) func TestMermaidOutput(t *testing.T) { - fsmUnderTest := NewFSM( + fsmUnderTest := New( "closed", - Flows[string, string]{ + Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "close", Src: []string{"open"}, Dst: "closed"}, {Event: "part-close", Src: []string{"intermediate"}, Dst: "closed"}, @@ -38,9 +38,9 @@ stateDiagram-v2 } func TestMermaidFlowChartOutput(t *testing.T) { - fsmUnderTest := NewFSM( + fsmUnderTest := New( "closed", - Flows[string, string]{ + Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, {Event: "part-open", Src: []string{"closed"}, Dst: "intermediate"}, {Event: "part-open", Src: []string{"intermediate"}, Dst: "open"}, From 978d788b1ccf15ee687a6e3316dc1cba5b0f3e15 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 14:50:52 +0200 Subject: [PATCH 12/33] Add benchmarks --- fsm_test.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/fsm_test.go b/fsm_test.go index 0307065..ddd4e8a 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -938,3 +938,38 @@ func ExampleFSM_Event_Generic() { // open // closed } + +func BenchmarkGenericFSM(b *testing.B) { + fsm := New( + IsClosed, + Transitions[MyEvent, MyState]{ + {Event: Open, Src: []MyState{IsClosed}, Dst: IsOpen}, + {Event: Close, Src: []MyState{IsOpen}, Dst: IsClosed}, + }, + Callbacks[MyEvent, MyState]{ + Any: func(cr *CallbackContext[MyEvent, MyState]) { + + }, + }, + ) + for i := 0; i < b.N; i++ { + fsm.Event(Open) + } +} +func BenchmarkFSM(b *testing.B) { + fsm := New( + "closed", + Transitions[string, string]{ + {Event: "open", Src: []string{"closed"}, Dst: "open"}, + {Event: "close", Src: []string{"open"}, Dst: "closed"}, + }, + Callbacks[string, string]{ + "": func(cr *CallbackContext[string, string]) { + + }, + }, + ) + for i := 0; i < b.N; i++ { + fsm.Event("open") + } +} From 49ce321b1f3bd4045312767eb2e19fddbd27a8a1 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 15:06:48 +0200 Subject: [PATCH 13/33] fix example --- fsm_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fsm_test.go b/fsm_test.go index ddd4e8a..6587c08 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -708,7 +708,7 @@ func TestNoTransition(t *testing.T) { } } -func ExampleNewFSM() { +func ExampleNew() { fsm := New( "green", Transitions[string, string]{ From c62ba1b3aaaaaea9ec4bc2a9e4b4175bb5884dd1 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Thu, 7 Jul 2022 15:12:46 +0200 Subject: [PATCH 14/33] Even more renamings --- fsm.go | 24 ++++++++++++------------ fsm_test.go | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/fsm.go b/fsm.go index d95463c..d093df3 100644 --- a/fsm.go +++ b/fsm.go @@ -50,8 +50,8 @@ type FSM[E Event, S State] struct { // transition is the internal transition functions used either directly // or when Transition is called in an asynchronous state transition. transition func() - // transitionerObj calls the FSM's transition() function. - transitionerObj transitioner[E, S] + // transitioner calls the FSM's transition() function. + transitioner transitioner[E, S] // stateMu guards access to the current state. stateMu sync.RWMutex @@ -70,7 +70,7 @@ type FSM[E Event, S State] struct { // the transition. If the FSM is in one of the source states it will end up in // the specified destination state, calling all defined callbacks as it goes. type Transition[E Event, S State] struct { - // Event is the event name used when calling for a transition. + // Event is the event used when calling for a transition. Event E // Src is a slice of source states that the FSM must be in to perform a @@ -130,11 +130,11 @@ type Callbacks[E Event, S State] map[E]Callback[E, S] // currently performed. func New[E Event, S State](initial S, transitions []Transition[E, S], callbacks map[E]Callback[E, S]) *FSM[E, S] { f := &FSM[E, S]{ - transitionerObj: &transitionerStruct[E, S]{}, - current: initial, - transitions: make(map[eKey[E, S]]S), - callbacks: make(map[cKey[E]]Callback[E, S]), - metadata: make(map[string]any), + transitioner: &defaultTransitioner[E, S]{}, + current: initial, + transitions: make(map[eKey[E, S]]S), + callbacks: make(map[cKey[E]]Callback[E, S]), + metadata: make(map[string]any), } // Build transition map and store sets of all events and states. @@ -357,18 +357,18 @@ func (f *FSM[E, S]) Transition() error { // doTransition wraps transitioner.transition. func (f *FSM[E, S]) doTransition() error { - return f.transitionerObj.transition(f) + return f.transitioner.transition(f) } -// transitionerStruct is the default implementation of the transitioner +// defaultTransitioner is the default implementation of the transitioner // interface. Other implementations can be swapped in for testing. -type transitionerStruct[E Event, S State] struct{} +type defaultTransitioner[E Event, S State] struct{} // Transition completes an asynchronous state change. // // The callback for leave_ must previously have called Async on its // event to have initiated an asynchronous state transition. -func (t transitionerStruct[E, S]) transition(f *FSM[E, S]) error { +func (t defaultTransitioner[E, S]) transition(f *FSM[E, S]) error { if f.transition == nil { return NotInTransitionError{} } diff --git a/fsm_test.go b/fsm_test.go index 6587c08..fad30de 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -22,10 +22,10 @@ import ( "time" ) -type fakeTransitionerObj[E Event, S State] struct { +type fakeTransitioner[E Event, S State] struct { } -func (t fakeTransitionerObj[E, S]) transition(f *FSM[E, S]) error { +func (t fakeTransitioner[E, S]) transition(f *FSM[E, S]) error { return &InternalError{} } @@ -69,7 +69,7 @@ func TestBadTransition(t *testing.T) { }, Callbacks[string, string]{}, ) - fsm.transitionerObj = new(fakeTransitionerObj[string, string]) + fsm.transitioner = new(fakeTransitioner[string, string]) err := fsm.Event("run") if err == nil { t.Error("bad transition should give an error") From aa1a0dcf1092074f8a70e8925db1b977b2c45a07 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sat, 9 Jul 2022 13:44:35 +0200 Subject: [PATCH 15/33] Generic Callbacks --- examples/alternate.go | 20 +-- examples/generic.go | 14 ++- fsm.go | 239 ++++++++++++++---------------------- fsm_test.go | 277 +++++++++++++++++++++++++++--------------- 4 files changed, 290 insertions(+), 260 deletions(-) diff --git a/examples/alternate.go b/examples/alternate.go index 0fd4ee1..f1064de 100644 --- a/examples/alternate.go +++ b/examples/alternate.go @@ -11,7 +11,7 @@ import ( func main() { - fsm := fsm.New( + f := fsm.New( "idle", fsm.Transistions[string, string]{ {Event: "scan", Src: []string{"idle"}, Dst: "scanning"}, @@ -36,34 +36,34 @@ func main() { }, ) - fmt.Println(fsm.Current()) + fmt.Println(f.Current()) - err := fsm.Event("scan") + err := f.Event("scan") if err != nil { fmt.Println(err) } - fmt.Println("1:" + fsm.Current()) + fmt.Println("1:" + f.Current()) - err = fsm.Event("working") + err = f.Event("working") if err != nil { fmt.Println(err) } - fmt.Println("2:" + fsm.Current()) + fmt.Println("2:" + f.Current()) - err = fsm.Event("situation") + err = f.Event("situation") if err != nil { fmt.Println(err) } - fmt.Println("3:" + fsm.Current()) + fmt.Println("3:" + f.Current()) - err = fsm.Event("finish") + err = f.Event("finish") if err != nil { fmt.Println(err) } - fmt.Println("4:" + fsm.Current()) + fmt.Println("4:" + f.Current()) } diff --git a/examples/generic.go b/examples/generic.go index 5f0c80d..28db8a1 100644 --- a/examples/generic.go +++ b/examples/generic.go @@ -26,11 +26,17 @@ func main() { {Event: Close, Src: []MyState{IsOpen}, Dst: IsClosed}, }, fsm.Callbacks[MyEvent, MyState]{ - Open: func(cr *fsm.CallbackContext[MyEvent, MyState]) { - fmt.Printf("callback: event:%s src:%s dst:%s\n", cr.Event, cr.Src, cr.Dst) + fsm.Callback[MyEvent, MyState]{ + When: fsm.AfterEvent, Event: Open, + F: func(cr *fsm.CallbackContext[MyEvent, MyState]) { + fmt.Printf("callback: event:%s src:%s dst:%s\n", cr.Event, cr.Src, cr.Dst) + }, }, - Any: func(cr *fsm.CallbackContext[MyEvent, MyState]) { - fmt.Printf("callback: event:%s src:%s dst:%s\n", cr.Event, cr.Src, cr.Dst) + fsm.Callback[MyEvent, MyState]{ + When: fsm.AfterAllEvents, + F: func(cr *fsm.CallbackContext[MyEvent, MyState]) { + fmt.Printf("callback after all: event:%s src:%s dst:%s\n", cr.Event, cr.Src, cr.Dst) + }, }, }, ) diff --git a/fsm.go b/fsm.go index d093df3..2368c12 100644 --- a/fsm.go +++ b/fsm.go @@ -25,7 +25,6 @@ package fsm import ( - "strings" "sync" ) @@ -45,7 +44,7 @@ type FSM[E Event, S State] struct { transitions map[eKey[E, S]]S // callbacks maps events and targets to callback functions. - callbacks map[cKey[E]]Callback[E, S] + callbacks Callbacks[E, S] // transition is the internal transition functions used either directly // or when Transition is called in an asynchronous state transition. @@ -84,13 +83,46 @@ type Transition[E Event, S State] struct { // Callback is a function type that callbacks should use. Event is the current // event info as the callback happens. -type Callback[E Event, S State] func(*CallbackContext[E, S]) +// type Callback[E Event, S State] func(*CallbackContext[E, S]) // Transitions is a shorthand for defining the transition map in NewFSM. type Transitions[E Event, S State] []Transition[E, S] // Callbacks is a shorthand for defining the callbacks in NewFSM. -type Callbacks[E Event, S State] map[E]Callback[E, S] +type Callbacks[E Event, S State] []Callback[E, S] + +// CallbackType defines at which type of Event this callback should be called. +type CallbackType int + +const ( + // BeforeEvent called before event E + BeforeEvent CallbackType = iota + // BeforeAllEvents called before all events + BeforeAllEvents + // AfterEvent called after event E + AfterEvent + // AfterAllEvents called after all events + AfterAllEvents + // EnterState called after entering state S + EnterState + // EnterAllStates called after entering all states + EnterAllStates + // LeaveState is called before leaving state S. + LeaveState + // LeaveAllStates is called before leaving all states. + LeaveAllStates +) + +type Callback[E Event, S State] struct { + // When should the callback be called. + When CallbackType + // Event is the event that the callback should be called for. Only relevant for BeforeEvent and AfterEvent. + Event E + // State is the state that the callback should be called for. Only relevant for EnterState and LeaveState. + State S + // F is the callback function. + F func(*CallbackContext[E, S]) +} // New constructs a FSM from events and callbacks. // @@ -98,107 +130,21 @@ type Callbacks[E Event, S State] map[E]Callback[E, S] // specified as Events. Each Event is mapped to one or more internal // transitions from Event.Src to Event.Dst. // -// Callbacks are added as a map specified as Callbacks where the key is parsed -// as the callback event as follows, and called in the same order: -// -// 1. before_ - called before event named -// -// 2. before_event - called before all events -// -// 3. leave_ - called before leaving -// -// 4. leave_state - called before leaving all states -// -// 5. enter_ - called after entering -// -// 6. enter_state - called after entering all states -// -// 7. after_ - called after event named -// -// 8. after_event - called after all events -// -// There are also two short form versions for the most commonly used callbacks. -// They are simply the name of the event or state: -// -// 1. - called after entering -// -// 2. - called after event named -// -// If both a shorthand version and a full version is specified it is undefined -// which version of the callback will end up in the internal map. This is due -// to the pseudo random nature of Go maps. No checking for multiple keys is -// currently performed. -func New[E Event, S State](initial S, transitions []Transition[E, S], callbacks map[E]Callback[E, S]) *FSM[E, S] { +// Callbacks are added as a slice specified as Callbacks and called in the same order. +func New[E Event, S State](initial S, transitions Transitions[E, S], callbacks Callbacks[E, S]) *FSM[E, S] { f := &FSM[E, S]{ - transitioner: &defaultTransitioner[E, S]{}, current: initial, - transitions: make(map[eKey[E, S]]S), - callbacks: make(map[cKey[E]]Callback[E, S]), - metadata: make(map[string]any), + transitioner: &defaultTransitioner[E, S]{}, + transitions: map[eKey[E, S]]S{}, + callbacks: callbacks, + metadata: map[string]any{}, } // Build transition map and store sets of all events and states. - allEvents := make(map[E]bool) - allStates := make(map[S]bool) for _, e := range transitions { for _, src := range e.Src { + // FIXME eKey still required? f.transitions[eKey[E, S]{e.Event, src}] = e.Dst - allStates[src] = true - allStates[e.Dst] = true - } - allEvents[e.Event] = true - } - - // Map all callbacks to events/states. - for event, fn := range callbacks { - var target string - var callbackType int - - eventName := string(event) // FIXME - switch { - case strings.HasPrefix(eventName, "before_"): - target = strings.TrimPrefix(eventName, "before_") - if target == "event" { - target = "" - callbackType = callbackBeforeEvent - } else if _, ok := allEvents[E(target)]; ok { // FIXME - callbackType = callbackBeforeEvent - } - case strings.HasPrefix(eventName, "leave_"): - target = strings.TrimPrefix(eventName, "leave_") - if target == "state" { - target = "" - callbackType = callbackLeaveState - } else if _, ok := allStates[S(target)]; ok { - callbackType = callbackLeaveState - } - case strings.HasPrefix(eventName, "enter_"): - target = strings.TrimPrefix(eventName, "enter_") - if target == "state" { - target = "" - callbackType = callbackEnterState - } else if _, ok := allStates[S(target)]; ok { - callbackType = callbackEnterState - } - case strings.HasPrefix(eventName, "after_"): - target = strings.TrimPrefix(eventName, "after_") - if target == "event" { - target = "" - callbackType = callbackAfterEvent - } else if _, ok := allEvents[E(target)]; ok { // FIXME - callbackType = callbackAfterEvent - } - default: - target = eventName - if _, ok := allStates[S(target)]; ok { - callbackType = callbackEnterState - } else if _, ok := allEvents[E(target)]; ok { // FIXME - callbackType = callbackAfterEvent - } - } - - if callbackType != callbackNone { - f.callbacks[cKey[E]{E(target), callbackType}] = fn // FIXME } } @@ -380,16 +326,20 @@ func (t defaultTransitioner[E, S]) transition(f *FSM[E, S]) error { // beforeEventCallbacks calls the before_ callbacks, first the named then the // general version. func (f *FSM[E, S]) beforeEventCallbacks(e *CallbackContext[E, S]) error { - if fn, ok := f.callbacks[cKey[E]{e.Event, callbackBeforeEvent}]; ok { - fn(e) - if e.canceled { - return CanceledError{e.Err} + for _, cb := range f.callbacks { + if cb.When == BeforeEvent { + if cb.Event == e.Event { + cb.F(e) + if e.canceled { + return CanceledError{e.Err} + } + } } - } - if fn, ok := f.callbacks[cKey[E]{"", callbackBeforeEvent}]; ok { - fn(e) - if e.canceled { - return CanceledError{e.Err} + if cb.When == BeforeAllEvents { + cb.F(e) + if e.canceled { + return CanceledError{e.Err} + } } } return nil @@ -398,20 +348,24 @@ func (f *FSM[E, S]) beforeEventCallbacks(e *CallbackContext[E, S]) error { // leaveStateCallbacks calls the leave_ callbacks, first the named then the // general version. func (f *FSM[E, S]) leaveStateCallbacks(e *CallbackContext[E, S]) error { - if fn, ok := f.callbacks[cKey[E]{E(f.current), callbackLeaveState}]; ok { // FIXME - fn(e) - if e.canceled { - return CanceledError{e.Err} - } else if e.async { - return AsyncError{e.Err} + for _, cb := range f.callbacks { + if cb.When == LeaveState { + if cb.State == e.Src { + cb.F(e) + if e.canceled { + return CanceledError{e.Err} + } else if e.async { + return AsyncError{e.Err} + } + } } - } - if fn, ok := f.callbacks[cKey[E]{"", callbackLeaveState}]; ok { - fn(e) - if e.canceled { - return CanceledError{e.Err} - } else if e.async { - return AsyncError{e.Err} + if cb.When == LeaveAllStates { + cb.F(e) + if e.canceled { + return CanceledError{e.Err} + } else if e.async { + return AsyncError{e.Err} + } } } return nil @@ -420,44 +374,33 @@ func (f *FSM[E, S]) leaveStateCallbacks(e *CallbackContext[E, S]) error { // enterStateCallbacks calls the enter_ callbacks, first the named then the // general version. func (f *FSM[E, S]) enterStateCallbacks(e *CallbackContext[E, S]) { - if fn, ok := f.callbacks[cKey[E]{E(f.current), callbackEnterState}]; ok { // FIXME - fn(e) - } - if fn, ok := f.callbacks[cKey[E]{"", callbackEnterState}]; ok { - fn(e) + for _, cb := range f.callbacks { + if cb.When == EnterState { + if cb.State == e.Dst { + cb.F(e) + } + } + if cb.When == EnterAllStates { + cb.F(e) + } } } // afterEventCallbacks calls the after_ callbacks, first the named then the // general version. func (f *FSM[E, S]) afterEventCallbacks(e *CallbackContext[E, S]) { - if fn, ok := f.callbacks[cKey[E]{e.Event, callbackAfterEvent}]; ok { - fn(e) - } - if fn, ok := f.callbacks[cKey[E]{"", callbackAfterEvent}]; ok { - fn(e) + for _, cb := range f.callbacks { + if cb.When == AfterEvent { + if cb.Event == e.Event { + cb.F(e) + } + } + if cb.When == AfterAllEvents { + cb.F(e) + } } } -const ( - callbackNone int = iota - callbackBeforeEvent - callbackLeaveState - callbackEnterState - callbackAfterEvent -) - -// cKey is a struct key used for keeping the callbacks mapped to a target. -type cKey[ES EventOrState] struct { // FIXME Type - // target is either the name of a state or an event depending on which - // callback type the key refers to. It can also be "" for a non-targeted - // callback like before_event. - target ES - - // callbackType is the situation when the callback will be run. - callbackType int -} - // eKey is a struct key used for storing the transition map. type eKey[E Event, S State] struct { // event is the name of the event that the keys refers to. diff --git a/fsm_test.go b/fsm_test.go index fad30de..3a6719d 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -215,17 +215,25 @@ func TestGenericCallbacks(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "before_event": func(e *CallbackContext[string, string]) { - beforeEvent = true + Callback[string, string]{When: BeforeAllEvents, + F: func(e *CallbackContext[string, string]) { + beforeEvent = true + }, }, - "leave_state": func(e *CallbackContext[string, string]) { - leaveState = true + Callback[string, string]{When: LeaveAllStates, + F: func(e *CallbackContext[string, string]) { + leaveState = true + }, }, - "enter_state": func(e *CallbackContext[string, string]) { - enterState = true + Callback[string, string]{When: EnterAllStates, + F: func(e *CallbackContext[string, string]) { + enterState = true + }, }, - "after_event": func(e *CallbackContext[string, string]) { - afterEvent = true + Callback[string, string]{When: AfterAllEvents, + F: func(e *CallbackContext[string, string]) { + afterEvent = true + }, }, }, ) @@ -251,17 +259,25 @@ func TestSpecificCallbacks(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "before_run": func(e *CallbackContext[string, string]) { - beforeEvent = true + Callback[string, string]{When: BeforeEvent, Event: "run", + F: func(e *CallbackContext[string, string]) { + beforeEvent = true + }, }, - "leave_start": func(e *CallbackContext[string, string]) { - leaveState = true + Callback[string, string]{When: LeaveState, State: "start", + F: func(e *CallbackContext[string, string]) { + leaveState = true + }, }, - "enter_end": func(e *CallbackContext[string, string]) { - enterState = true + Callback[string, string]{When: EnterState, State: "end", + F: func(e *CallbackContext[string, string]) { + enterState = true + }, }, - "after_run": func(e *CallbackContext[string, string]) { - afterEvent = true + Callback[string, string]{When: AfterEvent, Event: "run", + F: func(e *CallbackContext[string, string]) { + afterEvent = true + }, }, }, ) @@ -285,11 +301,15 @@ func TestSpecificCallbacksShortform(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "end": func(e *CallbackContext[string, string]) { - enterState = true + Callback[string, string]{When: EnterState, State: "end", + F: func(e *CallbackContext[string, string]) { + enterState = true + }, }, - "run": func(e *CallbackContext[string, string]) { - afterEvent = true + Callback[string, string]{When: AfterEvent, Event: "run", + F: func(e *CallbackContext[string, string]) { + afterEvent = true + }, }, }, ) @@ -312,8 +332,10 @@ func TestBeforeEventWithoutTransition(t *testing.T) { {Event: "dontrun", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string, string]{ - "before_event": func(e *CallbackContext[string, string]) { - beforeEvent = true + Callback[string, string]{When: BeforeAllEvents, + F: func(e *CallbackContext[string, string]) { + beforeEvent = true + }, }, }, ) @@ -338,8 +360,10 @@ func TestCancelBeforeGenericEvent(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "before_event": func(e *CallbackContext[string, string]) { - e.Cancel() + Callback[string, string]{When: BeforeAllEvents, + F: func(e *CallbackContext[string, string]) { + e.Cancel() + }, }, }, ) @@ -356,8 +380,10 @@ func TestCancelBeforeSpecificEvent(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "before_run": func(e *CallbackContext[string, string]) { - e.Cancel() + Callback[string, string]{When: BeforeEvent, Event: "run", + F: func(e *CallbackContext[string, string]) { + e.Cancel() + }, }, }, ) @@ -374,8 +400,10 @@ func TestCancelLeaveGenericState(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "leave_state": func(e *CallbackContext[string, string]) { - e.Cancel() + Callback[string, string]{When: LeaveState, State: "start", + F: func(e *CallbackContext[string, string]) { + e.Cancel() + }, }, }, ) @@ -392,8 +420,10 @@ func TestCancelLeaveSpecificState(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "leave_start": func(e *CallbackContext[string, string]) { - e.Cancel() + Callback[string, string]{When: LeaveState, State: "start", + F: func(e *CallbackContext[string, string]) { + e.Cancel() + }, }, }, ) @@ -410,8 +440,10 @@ func TestCancelWithError(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "before_event": func(e *CallbackContext[string, string]) { - e.Cancel(fmt.Errorf("error")) + Callback[string, string]{When: BeforeAllEvents, + F: func(e *CallbackContext[string, string]) { + e.Cancel(fmt.Errorf("error")) + }, }, }, ) @@ -436,8 +468,10 @@ func TestAsyncTransitionGenericState(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "leave_state": func(e *CallbackContext[string, string]) { - e.Async() + Callback[string, string]{When: LeaveState, State: "start", + F: func(e *CallbackContext[string, string]) { + e.Async() + }, }, }, ) @@ -461,8 +495,10 @@ func TestAsyncTransitionSpecificState(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "leave_start": func(e *CallbackContext[string, string]) { - e.Async() + Callback[string, string]{When: LeaveState, State: "start", + F: func(e *CallbackContext[string, string]) { + e.Async() + }, }, }, ) @@ -487,8 +523,10 @@ func TestAsyncTransitionInProgress(t *testing.T) { {Event: "reset", Src: []string{"end"}, Dst: "start"}, }, Callbacks[string, string]{ - "leave_start": func(e *CallbackContext[string, string]) { - e.Async() + Callback[string, string]{When: LeaveState, State: "start", + F: func(e *CallbackContext[string, string]) { + e.Async() + }, }, }, ) @@ -532,7 +570,9 @@ func TestCallbackNoError(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "run": func(e *CallbackContext[string, string]) { + Callback[string, string]{When: BeforeEvent, Event: "run", + F: func(e *CallbackContext[string, string]) { + }, }, }, ) @@ -549,8 +589,10 @@ func TestCallbackError(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "run": func(e *CallbackContext[string, string]) { - e.Err = fmt.Errorf("error") + Callback[string, string]{When: BeforeEvent, Event: "run", + F: func(e *CallbackContext[string, string]) { + e.Err = fmt.Errorf("error") + }, }, }, ) @@ -567,17 +609,19 @@ func TestCallbackArgs(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "run": func(e *CallbackContext[string, string]) { - if len(e.Args) != 1 { - t.Error("too few arguments") - } - arg, ok := e.Args[0].(string) - if !ok { - t.Error("not a string argument") - } - if arg != "test" { - t.Error("incorrect argument") - } + Callback[string, string]{When: BeforeEvent, Event: "run", + F: func(e *CallbackContext[string, string]) { + if len(e.Args) != 1 { + t.Error("too few arguments") + } + arg, ok := e.Args[0].(string) + if !ok { + t.Error("not a string argument") + } + if arg != "test" { + t.Error("incorrect argument") + } + }, }, }, ) @@ -601,8 +645,10 @@ func TestCallbackPanic(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "run": func(e *CallbackContext[string, string]) { - panic(panicMsg) + Callback[string, string]{When: BeforeEvent, Event: "run", + F: func(e *CallbackContext[string, string]) { + panic(panicMsg) + }, }, }, ) @@ -620,8 +666,10 @@ func TestNoDeadLock(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "run": func(e *CallbackContext[string, string]) { - fsm.Current() // Should not result in a panic / deadlock + Callback[string, string]{When: BeforeEvent, Event: "run", + F: func(e *CallbackContext[string, string]) { + fsm.Current() // Should not result in a panic / deadlock + }, }, }, ) @@ -638,7 +686,9 @@ func TestThreadSafetyRaceCondition(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "run": func(e *CallbackContext[string, string]) { + Callback[string, string]{When: BeforeEvent, Event: "run", + F: func(e *CallbackContext[string, string]) { + }, }, }, ) @@ -665,26 +715,28 @@ func TestDoubleTransition(t *testing.T) { {Event: "run", Src: []string{"start"}, Dst: "end"}, }, Callbacks[string, string]{ - "before_run": func(e *CallbackContext[string, string]) { - wg.Done() - // Imagine a concurrent event coming in of the same type while - // the data access mutex is unlocked because the current transition - // is running its event callbacks, getting around the "active" - // transition checks - if len(e.Args) == 0 { - // Must be concurrent so the test may pass when we add a mutex that synchronizes - // calls to Event(...). It will then fail as an inappropriate transition as we - // have changed state. - go func() { - if err := fsm.Event("run", "second run"); err != nil { - fmt.Println(err) - wg.Done() // It should fail, and then we unfreeze the test. - } - }() - time.Sleep(20 * time.Millisecond) - } else { - panic("Was able to reissue an event mid-transition") - } + Callback[string, string]{When: BeforeEvent, Event: "run", + F: func(e *CallbackContext[string, string]) { + wg.Done() + // Imagine a concurrent event coming in of the same type while + // the data access mutex is unlocked because the current transition + // is running its event callbacks, getting around the "active" + // transition checks + if len(e.Args) == 0 { + // Must be concurrent so the test may pass when we add a mutex that synchronizes + // calls to Event(...). It will then fail as an inappropriate transition as we + // have changed state. + go func() { + if err := fsm.Event("run", "second run"); err != nil { + fmt.Println(err) + wg.Done() // It should fail, and then we unfreeze the test. + } + }() + time.Sleep(20 * time.Millisecond) + } else { + panic("Was able to reissue an event mid-transition") + } + }, }, }, ) @@ -719,29 +771,45 @@ func ExampleNew() { {Event: "clear", Src: []string{"yellow"}, Dst: "green"}, }, Callbacks[string, string]{ - "before_warn": func(e *CallbackContext[string, string]) { - fmt.Println("before_warn") + Callback[string, string]{When: BeforeEvent, Event: "warn", + F: func(cc *CallbackContext[string, string]) { + fmt.Println("before_warn") + }, }, - "before_event": func(e *CallbackContext[string, string]) { - fmt.Println("before_event") + Callback[string, string]{When: BeforeAllEvents, + F: func(cc *CallbackContext[string, string]) { + fmt.Println("before_event") + }, }, - "leave_green": func(e *CallbackContext[string, string]) { - fmt.Println("leave_green") + Callback[string, string]{When: LeaveState, State: "green", + F: func(cc *CallbackContext[string, string]) { + fmt.Println("leave_green") + }, }, - "leave_state": func(e *CallbackContext[string, string]) { - fmt.Println("leave_state") + Callback[string, string]{When: LeaveAllStates, + F: func(cc *CallbackContext[string, string]) { + fmt.Println("leave_state") + }, }, - "enter_yellow": func(e *CallbackContext[string, string]) { - fmt.Println("enter_yellow") + Callback[string, string]{When: EnterState, State: "yellow", + F: func(cc *CallbackContext[string, string]) { + fmt.Println("enter_yellow") + }, }, - "enter_state": func(e *CallbackContext[string, string]) { - fmt.Println("enter_state") + Callback[string, string]{When: EnterAllStates, + F: func(cc *CallbackContext[string, string]) { + fmt.Println("enter_state") + }, }, - "after_warn": func(e *CallbackContext[string, string]) { - fmt.Println("after_warn") + Callback[string, string]{When: AfterEvent, Event: "warn", + F: func(cc *CallbackContext[string, string]) { + fmt.Println("after_warn") + }, }, - "after_event": func(e *CallbackContext[string, string]) { - fmt.Println("after_event") + Callback[string, string]{When: AfterAllEvents, + F: func(cc *CallbackContext[string, string]) { + fmt.Println("after_event") + }, }, }, ) @@ -877,8 +945,11 @@ func ExampleFSM_Transition() { {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, Callbacks[string, string]{ - "leave_closed": func(e *CallbackContext[string, string]) { - e.Async() + Callback[string, string]{ + When: LeaveState, State: "closed", + F: func(cc *CallbackContext[string, string]) { + cc.Async() + }, }, }, ) @@ -917,8 +988,11 @@ func ExampleFSM_Event_Generic() { {Event: Close, Src: []MyState{IsOpen}, Dst: IsClosed}, }, Callbacks[MyEvent, MyState]{ - Any: func(cr *CallbackContext[MyEvent, MyState]) { + Callback[MyEvent, MyState]{ + When: BeforeEvent, + F: func(cc *CallbackContext[MyEvent, MyState]) { + }, }, }, ) @@ -947,8 +1021,12 @@ func BenchmarkGenericFSM(b *testing.B) { {Event: Close, Src: []MyState{IsOpen}, Dst: IsClosed}, }, Callbacks[MyEvent, MyState]{ - Any: func(cr *CallbackContext[MyEvent, MyState]) { + Callback[MyEvent, MyState]{ + When: BeforeEvent, + F: func(cc *CallbackContext[MyEvent, MyState]) { + + }, }, }, ) @@ -964,8 +1042,11 @@ func BenchmarkFSM(b *testing.B) { {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, Callbacks[string, string]{ - "": func(cr *CallbackContext[string, string]) { + Callback[string, string]{ + When: BeforeEvent, + F: func(cc *CallbackContext[string, string]) { + }, }, }, ) From eec2205cf4085d40c9858d0836211a442486272e Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sat, 9 Jul 2022 14:01:58 +0200 Subject: [PATCH 16/33] more generic event and state constraints, godoc --- event.go | 12 ++--------- fsm.go | 48 +++++++++++++++++++++--------------------- fsm_test.go | 4 +++- graphviz_visualizer.go | 14 ++++++------ mermaid_visualizer.go | 20 ++++++++++-------- visualizer.go | 7 +++--- 6 files changed, 52 insertions(+), 53 deletions(-) diff --git a/event.go b/event.go index c4d8c4e..fb39eee 100644 --- a/event.go +++ b/event.go @@ -14,18 +14,10 @@ package fsm -type Event interface { - ~string -} -type State interface { - ~string -} -type EventOrState interface { - Event | State -} +import "golang.org/x/exp/constraints" // CallbackContext is the info that get passed as a reference in the callbacks. -type CallbackContext[E Event, S State] struct { +type CallbackContext[E constraints.Ordered, S constraints.Ordered] struct { // FSM is an reference to the current FSM. FSM *FSM[E, S] diff --git a/fsm.go b/fsm.go index 2368c12..037bcb2 100644 --- a/fsm.go +++ b/fsm.go @@ -25,18 +25,22 @@ package fsm import ( + "fmt" "sync" + + "golang.org/x/exp/constraints" ) // transitioner is an interface for the FSM's transition function. -type transitioner[E Event, S State] interface { +type transitioner[E constraints.Ordered, S constraints.Ordered] interface { transition(*FSM[E, S]) error } // FSM is the state machine that holds the current state. -// -// It has to be created with NewFSM to function properly. -type FSM[E Event, S State] struct { +// E ist the event +// S is the state +// It has to be created with New to function properly. +type FSM[E constraints.Ordered, S constraints.Ordered] struct { // current is the state that the FSM is currently in. current S @@ -68,7 +72,7 @@ type FSM[E Event, S State] struct { // The event can have one or more source states that is valid for performing // the transition. If the FSM is in one of the source states it will end up in // the specified destination state, calling all defined callbacks as it goes. -type Transition[E Event, S State] struct { +type Transition[E constraints.Ordered, S constraints.Ordered] struct { // Event is the event used when calling for a transition. Event E @@ -81,15 +85,8 @@ type Transition[E Event, S State] struct { Dst S } -// Callback is a function type that callbacks should use. Event is the current -// event info as the callback happens. -// type Callback[E Event, S State] func(*CallbackContext[E, S]) - // Transitions is a shorthand for defining the transition map in NewFSM. -type Transitions[E Event, S State] []Transition[E, S] - -// Callbacks is a shorthand for defining the callbacks in NewFSM. -type Callbacks[E Event, S State] []Callback[E, S] +type Transitions[E constraints.Ordered, S constraints.Ordered] []Transition[E, S] // CallbackType defines at which type of Event this callback should be called. type CallbackType int @@ -113,7 +110,7 @@ const ( LeaveAllStates ) -type Callback[E Event, S State] struct { +type Callback[E constraints.Ordered, S constraints.Ordered] struct { // When should the callback be called. When CallbackType // Event is the event that the callback should be called for. Only relevant for BeforeEvent and AfterEvent. @@ -124,14 +121,17 @@ type Callback[E Event, S State] struct { F func(*CallbackContext[E, S]) } -// New constructs a FSM from events and callbacks. +// Callbacks is a shorthand for defining the callbacks in NewFSM. +type Callbacks[E constraints.Ordered, S constraints.Ordered] []Callback[E, S] + +// New constructs a generic FSM with a initial state S, for events E. +// E is the event type, S is the state type. // -// The events and transitions are specified as a slice of Event structs -// specified as Events. Each Event is mapped to one or more internal -// transitions from Event.Src to Event.Dst. +// Transistions define the state transistions that can be performed for a given event +// and a slice of source states, the destination state and the callback function. // // Callbacks are added as a slice specified as Callbacks and called in the same order. -func New[E Event, S State](initial S, transitions Transitions[E, S], callbacks Callbacks[E, S]) *FSM[E, S] { +func New[E constraints.Ordered, S constraints.Ordered](initial S, transitions Transitions[E, S], callbacks Callbacks[E, S]) *FSM[E, S] { f := &FSM[E, S]{ current: initial, transitioner: &defaultTransitioner[E, S]{}, @@ -241,17 +241,17 @@ func (f *FSM[E, S]) Event(event E, args ...any) error { defer f.stateMu.RUnlock() if f.transition != nil { - return InTransitionError{string(event)} + return InTransitionError{fmt.Sprintf("%v", event)} } dst, ok := f.transitions[eKey[E, S]{event, f.current}] if !ok { for ekey := range f.transitions { if ekey.event == event { - return InvalidEventError{string(event), string(f.current)} + return InvalidEventError{fmt.Sprintf("%v", event), fmt.Sprintf("%v", f.current)} } } - return UnknownEventError{string(event)} + return UnknownEventError{fmt.Sprintf("%v", event)} } e := &CallbackContext[E, S]{f, event, f.current, dst, nil, args, false, false} @@ -308,7 +308,7 @@ func (f *FSM[E, S]) doTransition() error { // defaultTransitioner is the default implementation of the transitioner // interface. Other implementations can be swapped in for testing. -type defaultTransitioner[E Event, S State] struct{} +type defaultTransitioner[E constraints.Ordered, S constraints.Ordered] struct{} // Transition completes an asynchronous state change. // @@ -402,7 +402,7 @@ func (f *FSM[E, S]) afterEventCallbacks(e *CallbackContext[E, S]) { } // eKey is a struct key used for storing the transition map. -type eKey[E Event, S State] struct { +type eKey[E constraints.Ordered, S constraints.Ordered] struct { // event is the name of the event that the keys refers to. event E diff --git a/fsm_test.go b/fsm_test.go index 3a6719d..5a5f2d9 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -20,9 +20,11 @@ import ( "sync" "testing" "time" + + "golang.org/x/exp/constraints" ) -type fakeTransitioner[E Event, S State] struct { +type fakeTransitioner[E constraints.Ordered, S constraints.Ordered] struct { } func (t fakeTransitioner[E, S]) transition(f *FSM[E, S]) error { diff --git a/graphviz_visualizer.go b/graphviz_visualizer.go index e00becf..62ec542 100644 --- a/graphviz_visualizer.go +++ b/graphviz_visualizer.go @@ -3,10 +3,12 @@ package fsm import ( "bytes" "fmt" + + "golang.org/x/exp/constraints" ) // Visualize outputs a visualization of a FSM in Graphviz format. -func Visualize[E Event, S State](fsm *FSM[E, S]) string { +func Visualize[E constraints.Ordered, S constraints.Ordered](fsm *FSM[E, S]) string { var buf bytes.Buffer // we sort the key alphabetically to have a reproducible graph output @@ -26,19 +28,19 @@ func writeHeaderLine(buf *bytes.Buffer) { buf.WriteString("\n") } -func writeTransitions[E Event, S State](buf *bytes.Buffer, current S, sortedEKeys []eKey[E, S], transitions map[eKey[E, S]]S) { +func writeTransitions[E constraints.Ordered, S constraints.Ordered](buf *bytes.Buffer, current S, sortedEKeys []eKey[E, S], transitions map[eKey[E, S]]S) { // make sure the current state is at top for _, k := range sortedEKeys { if k.src == current { v := transitions[k] - buf.WriteString(fmt.Sprintf(` "%s" -> "%s" [ label = "%s" ];`, k.src, v, k.event)) + buf.WriteString(fmt.Sprintf(` "%v" -> "%v" [ label = "%v" ];`, k.src, v, k.event)) buf.WriteString("\n") } } for _, k := range sortedEKeys { if k.src != current { v := transitions[k] - buf.WriteString(fmt.Sprintf(` "%s" -> "%s" [ label = "%s" ];`, k.src, v, k.event)) + buf.WriteString(fmt.Sprintf(` "%v" -> "%v" [ label = "%v" ];`, k.src, v, k.event)) buf.WriteString("\n") } } @@ -46,9 +48,9 @@ func writeTransitions[E Event, S State](buf *bytes.Buffer, current S, sortedEKey buf.WriteString("\n") } -func writeStates[S State](buf *bytes.Buffer, sortedStateKeys []S) { +func writeStates[S constraints.Ordered](buf *bytes.Buffer, sortedStateKeys []S) { for _, k := range sortedStateKeys { - buf.WriteString(fmt.Sprintf(` "%s";`, k)) + buf.WriteString(fmt.Sprintf(` "%v";`, k)) buf.WriteString("\n") } } diff --git a/mermaid_visualizer.go b/mermaid_visualizer.go index e566883..cd662b4 100644 --- a/mermaid_visualizer.go +++ b/mermaid_visualizer.go @@ -3,6 +3,8 @@ package fsm import ( "bytes" "fmt" + + "golang.org/x/exp/constraints" ) const highlightingColor = "#00AA00" @@ -18,7 +20,7 @@ const ( ) // VisualizeForMermaidWithGraphType outputs a visualization of a FSM in Mermaid format as specified by the graphType. -func VisualizeForMermaidWithGraphType[E Event, S State](fsm *FSM[E, S], graphType MermaidDiagramType) (string, error) { +func VisualizeForMermaidWithGraphType[E constraints.Ordered, S constraints.Ordered](fsm *FSM[E, S], graphType MermaidDiagramType) (string, error) { switch graphType { case FlowChart: return visualizeForMermaidAsFlowChart(fsm), nil @@ -29,7 +31,7 @@ func VisualizeForMermaidWithGraphType[E Event, S State](fsm *FSM[E, S], graphTyp } } -func visualizeForMermaidAsStateDiagram[E Event, S State](fsm *FSM[E, S]) string { +func visualizeForMermaidAsStateDiagram[E constraints.Ordered, S constraints.Ordered](fsm *FSM[E, S]) string { var buf bytes.Buffer sortedTransitionKeys := getSortedTransitionKeys(fsm.transitions) @@ -39,7 +41,7 @@ func visualizeForMermaidAsStateDiagram[E Event, S State](fsm *FSM[E, S]) string for _, k := range sortedTransitionKeys { v := fsm.transitions[k] - buf.WriteString(fmt.Sprintf(` %s --> %s: %s`, k.src, v, k.event)) + buf.WriteString(fmt.Sprintf(` %v --> %v: %v`, k.src, v, k.event)) buf.WriteString("\n") } @@ -47,7 +49,7 @@ func visualizeForMermaidAsStateDiagram[E Event, S State](fsm *FSM[E, S]) string } // visualizeForMermaidAsFlowChart outputs a visualization of a FSM in Mermaid format (including highlighting of current state). -func visualizeForMermaidAsFlowChart[E Event, S State](fsm *FSM[E, S]) string { +func visualizeForMermaidAsFlowChart[E constraints.Ordered, S constraints.Ordered](fsm *FSM[E, S]) string { var buf bytes.Buffer sortedTransitionKeys := getSortedTransitionKeys(fsm.transitions) @@ -65,25 +67,25 @@ func writeFlowChartGraphType(buf *bytes.Buffer) { buf.WriteString("graph LR\n") } -func writeFlowChartStates[S State](buf *bytes.Buffer, sortedStates []S, statesToIDMap map[S]string) { +func writeFlowChartStates[S constraints.Ordered](buf *bytes.Buffer, sortedStates []S, statesToIDMap map[S]string) { for _, state := range sortedStates { - buf.WriteString(fmt.Sprintf(` %s[%s]`, statesToIDMap[state], state)) + buf.WriteString(fmt.Sprintf(` %s[%v]`, statesToIDMap[state], state)) buf.WriteString("\n") } buf.WriteString("\n") } -func writeFlowChartTransitions[E Event, S State](buf *bytes.Buffer, transitions map[eKey[E, S]]S, sortedTransitionKeys []eKey[E, S], statesToIDMap map[S]string) { +func writeFlowChartTransitions[E constraints.Ordered, S constraints.Ordered](buf *bytes.Buffer, transitions map[eKey[E, S]]S, sortedTransitionKeys []eKey[E, S], statesToIDMap map[S]string) { for _, transition := range sortedTransitionKeys { target := transitions[transition] - buf.WriteString(fmt.Sprintf(` %s --> |%s| %s`, statesToIDMap[transition.src], transition.event, statesToIDMap[target])) + buf.WriteString(fmt.Sprintf(` %s --> |%v| %s`, statesToIDMap[transition.src], transition.event, statesToIDMap[target])) buf.WriteString("\n") } buf.WriteString("\n") } -func writeFlowChartHighlightCurrent[S State](buf *bytes.Buffer, current S, statesToIDMap map[S]string) { +func writeFlowChartHighlightCurrent[S constraints.Ordered](buf *bytes.Buffer, current S, statesToIDMap map[S]string) { buf.WriteString(fmt.Sprintf(` style %s fill:%s`, statesToIDMap[current], highlightingColor)) buf.WriteString("\n") } diff --git a/visualizer.go b/visualizer.go index 509565b..44afa19 100644 --- a/visualizer.go +++ b/visualizer.go @@ -4,6 +4,7 @@ import ( "fmt" "sort" + "golang.org/x/exp/constraints" "golang.org/x/exp/slices" ) @@ -23,7 +24,7 @@ const ( // VisualizeWithType outputs a visualization of a FSM in the desired format. // If the type is not given it defaults to GRAPHVIZ -func VisualizeWithType[E Event, S State](fsm *FSM[E, S], visualizeType VisualizeType) (string, error) { +func VisualizeWithType[E constraints.Ordered, S constraints.Ordered](fsm *FSM[E, S], visualizeType VisualizeType) (string, error) { switch visualizeType { case GRAPHVIZ: return Visualize(fsm), nil @@ -38,7 +39,7 @@ func VisualizeWithType[E Event, S State](fsm *FSM[E, S], visualizeType Visualize } } -func getSortedTransitionKeys[E Event, S State](transitions map[eKey[E, S]]S) []eKey[E, S] { +func getSortedTransitionKeys[E constraints.Ordered, S constraints.Ordered](transitions map[eKey[E, S]]S) []eKey[E, S] { // we sort the key alphabetically to have a reproducible graph output sortedTransitionKeys := make([]eKey[E, S], 0) @@ -55,7 +56,7 @@ func getSortedTransitionKeys[E Event, S State](transitions map[eKey[E, S]]S) []e return sortedTransitionKeys } -func getSortedStates[E Event, S State](transitions map[eKey[E, S]]S) ([]S, map[S]string) { +func getSortedStates[E constraints.Ordered, S constraints.Ordered](transitions map[eKey[E, S]]S) ([]S, map[S]string) { statesToIDMap := make(map[S]string) for transition, target := range transitions { if _, ok := statesToIDMap[transition.src]; !ok { From 67d99fbf412d8aeb01dd02b1b0df6b18b708c9fd Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sat, 9 Jul 2022 14:16:07 +0200 Subject: [PATCH 17/33] Fix examples --- examples/alternate.go | 28 ++++++++++++++++------------ examples/data.go | 24 ++++++++++++------------ examples/struct.go | 4 +++- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/examples/alternate.go b/examples/alternate.go index f1064de..4af0f6a 100644 --- a/examples/alternate.go +++ b/examples/alternate.go @@ -1,6 +1,3 @@ -//go:build ignore -// +build ignore - package main import ( @@ -10,7 +7,6 @@ import ( ) func main() { - f := fsm.New( "idle", fsm.Transistions[string, string]{ @@ -21,17 +17,25 @@ func main() { {Event: "finish", Src: []string{"scanning"}, Dst: "idle"}, }, fsm.Callbacks[string, string]{ - "scan": func(e *fsm.CallbackContext[string, string]) { - fmt.Println("after_scan: " + e.FSM.Current()) + fsm.Callback[string, string]{When: fsm.BeforeEvent, Event: "scan", + F: func(e *fsm.CallbackContext[string, string]) { + fmt.Println("after_scan: " + e.FSM.Current()) + }, }, - "working": func(e *fsm.CallbackContext[string, string]) { - fmt.Println("working: " + e.FSM.Current()) + fsm.Callback[string, string]{When: fsm.BeforeEvent, Event: "working", + F: func(e *fsm.CallbackContext[string, string]) { + fmt.Println("working: " + e.FSM.Current()) + }, }, - "situation": func(e *fsm.CallbackContext[string, string]) { - fmt.Println("situation: " + e.FSM.Current()) + fsm.Callback[string, string]{When: fsm.BeforeEvent, Event: "situation", + F: func(e *fsm.CallbackContext[string, string]) { + fmt.Println("situation: " + e.FSM.Current()) + }, }, - "finish": func(e *fsm.CallbackContext[string, string]) { - fmt.Println("finish: " + e.FSM.Current()) + fsm.Callback[string, string]{When: fsm.BeforeEvent, Event: "finish", + F: func(e *fsm.CallbackContext[string, string]) { + fmt.Println("finish: " + e.FSM.Current()) + }, }, }, ) diff --git a/examples/data.go b/examples/data.go index a62ad3f..7baa013 100644 --- a/examples/data.go +++ b/examples/data.go @@ -1,6 +1,3 @@ -//go:build ignore -// +build ignore - package main import ( @@ -17,16 +14,19 @@ func main() { {Event: "consume", Src: []string{"idle"}, Dst: "idle"}, }, fsm.Callbacks[string, string]{ - "produce": func(e *fsm.CallbackContext[string, string]) { - e.FSM.SetMetadata("message", "hii") - fmt.Println("produced data") + fsm.Callback[string, string]{When: fsm.BeforeEvent, Event: "sproduce", + F: func(e *fsm.CallbackContext[string, string]) { + e.FSM.SetMetadata("message", "hii") + fmt.Println("produced data") + }, }, - "consume": func(e *fsm.CallbackContext[string, string]) { - message, ok := e.FSM.Metadata("message") - if ok { - fmt.Println("message = " + message.(string)) - } - + fsm.Callback[string, string]{When: fsm.BeforeEvent, Event: "consume", + F: func(e *fsm.CallbackContext[string, string]) { + message, ok := e.FSM.Metadata("message") + if ok { + fmt.Println("message = " + message.(string)) + } + }, }, }, ) diff --git a/examples/struct.go b/examples/struct.go index ed2e168..aa3ed1b 100644 --- a/examples/struct.go +++ b/examples/struct.go @@ -26,7 +26,9 @@ func NewDoor(to string) *Door { {Event: "close", Src: []string{"open"}, Dst: "closed"}, }, fsm.Callbacks[string, string]{ - "enter_state": func(e *fsm.CallbackContext[string, string]) { d.enterState(e) }, + fsm.Callback[string, string]{When: fsm.EnterAllStates, + F: func(e *fsm.CallbackContext[string, string]) { d.enterState(e) }, + }, }, ) From 2cedcae975f05ef063a831fd2b5c3cf283f0e578 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sat, 9 Jul 2022 14:18:09 +0200 Subject: [PATCH 18/33] Fix --- examples/alternate.go | 3 +++ examples/data.go | 3 +++ examples/generic.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/examples/alternate.go b/examples/alternate.go index 4af0f6a..1b7f5fd 100644 --- a/examples/alternate.go +++ b/examples/alternate.go @@ -1,3 +1,6 @@ +//go:build ignore +// +build ignore + package main import ( diff --git a/examples/data.go b/examples/data.go index 7baa013..baeb77c 100644 --- a/examples/data.go +++ b/examples/data.go @@ -1,3 +1,6 @@ +//go:build ignore +// +build ignore + package main import ( diff --git a/examples/generic.go b/examples/generic.go index 28db8a1..c212aa5 100644 --- a/examples/generic.go +++ b/examples/generic.go @@ -1,3 +1,6 @@ +//go:build ignore +// +build ignore + package main import ( From c8078b81c0ad71dd77f1db3659d16e51e98d5f3d Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sat, 9 Jul 2022 14:29:12 +0200 Subject: [PATCH 19/33] Fix readme --- README.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f11b126..1582966 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,13 @@ import ( ) func main() { - fsm := fsm.New( + fsm := fsm.New[string, string]( "closed", - fsm.Events{ + fsm.Events[string, string]{ {Name: "open", Src: []string{"closed"}, Dst: "open"}, {Name: "close", Src: []string{"open"}, Dst: "closed"}, }, - fsm.Callbacks{}, + fsm.Callbacks[string, string]{}, ) fmt.Println(fsm.Current()) @@ -77,14 +77,19 @@ func NewDoor(to string) *Door { To: to, } - d.FSM = fsm.New( + d.FSM = fsm.New[string, string]( "closed", - fsm.Events{ + fsm.Events[string, string]{ {Name: "open", Src: []string{"closed"}, Dst: "open"}, {Name: "close", Src: []string{"open"}, Dst: "closed"}, }, - fsm.Callbacks{ - "enter_state": func(e *fsm.Event) { d.enterState(e) }, + fsm.Callbacks[string, string]{ + fsm.Callback[string, string]{ + When: fsm.AfterAllStates, + F: func(cr *fsm.CallbackContext[MyEvent, MyState]) { + d.enterState(e) + }, + }, }, ) From 1e23f924cc36dbc3c767555e045d50226e8848a9 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sat, 9 Jul 2022 14:32:46 +0200 Subject: [PATCH 20/33] Linter --- fsm_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fsm_test.go b/fsm_test.go index 5a5f2d9..3647b3b 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -982,7 +982,7 @@ const ( IsOpen MyState = "open" ) -func ExampleFSM_Event_Generic() { +func ExampleFSM_Event_generic() { fsm := New( IsClosed, Transitions[MyEvent, MyState]{ @@ -1033,7 +1033,7 @@ func BenchmarkGenericFSM(b *testing.B) { }, ) for i := 0; i < b.N; i++ { - fsm.Event(Open) + _ = fsm.Event(Open) } } func BenchmarkFSM(b *testing.B) { @@ -1053,6 +1053,6 @@ func BenchmarkFSM(b *testing.B) { }, ) for i := 0; i < b.N; i++ { - fsm.Event("open") + _ = fsm.Event("open") } } From 31b81831cf785cec24ad974ea2499cf767964920 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sun, 10 Jul 2022 09:58:43 +0200 Subject: [PATCH 21/33] nameing --- event.go => callback.go | 50 ++++++++++++++++++++++++++++++++++----- fsm.go | 52 +++++++---------------------------------- 2 files changed, 52 insertions(+), 50 deletions(-) rename event.go => callback.go (56%) diff --git a/event.go b/callback.go similarity index 56% rename from event.go rename to callback.go index fb39eee..026c33f 100644 --- a/event.go +++ b/callback.go @@ -14,7 +14,45 @@ package fsm -import "golang.org/x/exp/constraints" +import ( + "golang.org/x/exp/constraints" +) + +// CallbackType defines at which type of Event this callback should be called. +type CallbackType int + +const ( + // BeforeEvent called before event E + BeforeEvent CallbackType = iota + // BeforeAllEvents called before all events + BeforeAllEvents + // AfterEvent called after event E + AfterEvent + // AfterAllEvents called after all events + AfterAllEvents + // EnterState called after entering state S + EnterState + // EnterAllStates called after entering all states + EnterAllStates + // LeaveState is called before leaving state S. + LeaveState + // LeaveAllStates is called before leaving all states. + LeaveAllStates +) + +type Callback[E constraints.Ordered, S constraints.Ordered] struct { + // When should the callback be called. + When CallbackType + // Event is the event that the callback should be called for. Only relevant for BeforeEvent and AfterEvent. + Event E + // State is the state that the callback should be called for. Only relevant for EnterState and LeaveState. + State S + // F is the callback function. + F func(*CallbackContext[E, S]) +} + +// Callbacks is a shorthand for defining the callbacks in NewFSM. +type Callbacks[E constraints.Ordered, S constraints.Ordered] []Callback[E, S] // CallbackContext is the info that get passed as a reference in the callbacks. type CallbackContext[E constraints.Ordered, S constraints.Ordered] struct { @@ -46,11 +84,11 @@ type CallbackContext[E constraints.Ordered, S constraints.Ordered] struct { // Cancel can be called in before_ or leave_ to cancel the // current transition before it happens. It takes an optional error, which will // overwrite e.Err if set before. -func (e *CallbackContext[E, S]) Cancel(err ...error) { - e.canceled = true +func (ctx *CallbackContext[E, S]) Cancel(err ...error) { + ctx.canceled = true if len(err) > 0 { - e.Err = err[0] + ctx.Err = err[0] } } @@ -59,6 +97,6 @@ func (e *CallbackContext[E, S]) Cancel(err ...error) { // The current state transition will be on hold in the old state until a final // call to Transition is made. This will complete the transition and possibly // call the other callbacks. -func (e *CallbackContext[E, S]) Async() { - e.async = true +func (ctx *CallbackContext[E, S]) Async() { + ctx.async = true } diff --git a/fsm.go b/fsm.go index 037bcb2..0d2e770 100644 --- a/fsm.go +++ b/fsm.go @@ -60,10 +60,11 @@ type FSM[E constraints.Ordered, S constraints.Ordered] struct { stateMu sync.RWMutex // eventMu guards access to Event() and Transition(). eventMu sync.Mutex + // metadata can be used to store and load data that maybe used across events // use methods SetMetadata() and Metadata() to store and load data metadata map[string]any - + // metadataMu guards access to the metadata. metadataMu sync.RWMutex } @@ -88,42 +89,6 @@ type Transition[E constraints.Ordered, S constraints.Ordered] struct { // Transitions is a shorthand for defining the transition map in NewFSM. type Transitions[E constraints.Ordered, S constraints.Ordered] []Transition[E, S] -// CallbackType defines at which type of Event this callback should be called. -type CallbackType int - -const ( - // BeforeEvent called before event E - BeforeEvent CallbackType = iota - // BeforeAllEvents called before all events - BeforeAllEvents - // AfterEvent called after event E - AfterEvent - // AfterAllEvents called after all events - AfterAllEvents - // EnterState called after entering state S - EnterState - // EnterAllStates called after entering all states - EnterAllStates - // LeaveState is called before leaving state S. - LeaveState - // LeaveAllStates is called before leaving all states. - LeaveAllStates -) - -type Callback[E constraints.Ordered, S constraints.Ordered] struct { - // When should the callback be called. - When CallbackType - // Event is the event that the callback should be called for. Only relevant for BeforeEvent and AfterEvent. - Event E - // State is the state that the callback should be called for. Only relevant for EnterState and LeaveState. - State S - // F is the callback function. - F func(*CallbackContext[E, S]) -} - -// Callbacks is a shorthand for defining the callbacks in NewFSM. -type Callbacks[E constraints.Ordered, S constraints.Ordered] []Callback[E, S] - // New constructs a generic FSM with a initial state S, for events E. // E is the event type, S is the state type. // @@ -147,7 +112,6 @@ func New[E constraints.Ordered, S constraints.Ordered](initial S, transitions Tr f.transitions[eKey[E, S]{e.Event, src}] = e.Dst } } - return f } @@ -181,6 +145,12 @@ func (f *FSM[E, S]) Can(event E) bool { return ok && (f.transition == nil) } +// Cannot returns true if event can not occur in the current state. +// It is a convenience method to help code read nicely. +func (f *FSM[E, S]) Cannot(event E) bool { + return !f.Can(event) +} + // AvailableTransitions returns a list of transitions available in the // current state. func (f *FSM[E, S]) AvailableTransitions() []E { @@ -195,12 +165,6 @@ func (f *FSM[E, S]) AvailableTransitions() []E { return transitions } -// Cannot returns true if event can not occur in the current state. -// It is a convenience method to help code read nicely. -func (f *FSM[E, S]) Cannot(event E) bool { - return !f.Can(event) -} - // Metadata returns the value stored in metadata func (f *FSM[E, S]) Metadata(key string) (any, bool) { f.metadataMu.RLock() From 3cc3d2cbc86cdf449fc887f903ecbe6805e7d6c3 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 11 Jul 2022 08:40:35 +0200 Subject: [PATCH 22/33] nameing --- errors.go | 14 ++++++------ fsm.go | 64 +++++++++++++++++++++++++++---------------------------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/errors.go b/errors.go index 9c32a49..011bda3 100644 --- a/errors.go +++ b/errors.go @@ -14,6 +14,8 @@ package fsm +import "fmt" + // InvalidEventError is returned by FSM.Event() when the event cannot be called // in the current state. type InvalidEventError struct { @@ -22,7 +24,7 @@ type InvalidEventError struct { } func (e InvalidEventError) Error() string { - return "event " + e.Event + " inappropriate in current state " + e.State + return fmt.Sprintf("event %s inappropriate in current state %s", e.Event, e.State) } // UnknownEventError is returned by FSM.Event() when the event is not defined. @@ -31,7 +33,7 @@ type UnknownEventError struct { } func (e UnknownEventError) Error() string { - return "event " + e.Event + " does not exist" + return fmt.Sprintf("event %s does not exist", e.Event) } // InTransitionError is returned by FSM.Event() when an asynchronous transition @@ -41,7 +43,7 @@ type InTransitionError struct { } func (e InTransitionError) Error() string { - return "event " + e.Event + " inappropriate because previous transition did not complete" + return fmt.Sprintf("event %s inappropriate because previous transition did not complete", e.Event) } // NotInTransitionError is returned by FSM.Transition() when an asynchronous @@ -60,7 +62,7 @@ type NoTransitionError struct { func (e NoTransitionError) Error() string { if e.Err != nil { - return "no transition with error: " + e.Err.Error() + return fmt.Sprintf("no transition with error: %s", e.Err.Error()) } return "no transition" } @@ -73,7 +75,7 @@ type CanceledError struct { func (e CanceledError) Error() string { if e.Err != nil { - return "transition canceled with error: " + e.Err.Error() + return fmt.Sprintf("transition canceled with error: %s", e.Err.Error()) } return "transition canceled" } @@ -86,7 +88,7 @@ type AsyncError struct { func (e AsyncError) Error() string { if e.Err != nil { - return "async started with error: " + e.Err.Error() + return fmt.Sprintf("async started with error: %s", e.Err.Error()) } return "async started" } diff --git a/fsm.go b/fsm.go index 0d2e770..063160a 100644 --- a/fsm.go +++ b/fsm.go @@ -287,80 +287,80 @@ func (t defaultTransitioner[E, S]) transition(f *FSM[E, S]) error { return nil } -// beforeEventCallbacks calls the before_ callbacks, first the named then the +// beforeEventCallbacks calls the before callbacks, first the named then the // general version. -func (f *FSM[E, S]) beforeEventCallbacks(e *CallbackContext[E, S]) error { +func (f *FSM[E, S]) beforeEventCallbacks(cc *CallbackContext[E, S]) error { for _, cb := range f.callbacks { if cb.When == BeforeEvent { - if cb.Event == e.Event { - cb.F(e) - if e.canceled { - return CanceledError{e.Err} + if cb.Event == cc.Event { + cb.F(cc) + if cc.canceled { + return CanceledError{cc.Err} } } } if cb.When == BeforeAllEvents { - cb.F(e) - if e.canceled { - return CanceledError{e.Err} + cb.F(cc) + if cc.canceled { + return CanceledError{cc.Err} } } } return nil } -// leaveStateCallbacks calls the leave_ callbacks, first the named then the +// leaveStateCallbacks calls the leave callbacks, first the named then the // general version. -func (f *FSM[E, S]) leaveStateCallbacks(e *CallbackContext[E, S]) error { +func (f *FSM[E, S]) leaveStateCallbacks(cc *CallbackContext[E, S]) error { for _, cb := range f.callbacks { if cb.When == LeaveState { - if cb.State == e.Src { - cb.F(e) - if e.canceled { - return CanceledError{e.Err} - } else if e.async { - return AsyncError{e.Err} + if cb.State == cc.Src { + cb.F(cc) + if cc.canceled { + return CanceledError{cc.Err} + } else if cc.async { + return AsyncError{cc.Err} } } } if cb.When == LeaveAllStates { - cb.F(e) - if e.canceled { - return CanceledError{e.Err} - } else if e.async { - return AsyncError{e.Err} + cb.F(cc) + if cc.canceled { + return CanceledError{cc.Err} + } else if cc.async { + return AsyncError{cc.Err} } } } return nil } -// enterStateCallbacks calls the enter_ callbacks, first the named then the +// enterStateCallbacks calls the enter callbacks, first the named then the // general version. -func (f *FSM[E, S]) enterStateCallbacks(e *CallbackContext[E, S]) { +func (f *FSM[E, S]) enterStateCallbacks(cc *CallbackContext[E, S]) { for _, cb := range f.callbacks { if cb.When == EnterState { - if cb.State == e.Dst { - cb.F(e) + if cb.State == cc.Dst { + cb.F(cc) } } if cb.When == EnterAllStates { - cb.F(e) + cb.F(cc) } } } -// afterEventCallbacks calls the after_ callbacks, first the named then the +// afterEventCallbacks calls the after callbacks, first the named then the // general version. -func (f *FSM[E, S]) afterEventCallbacks(e *CallbackContext[E, S]) { +func (f *FSM[E, S]) afterEventCallbacks(cc *CallbackContext[E, S]) { for _, cb := range f.callbacks { if cb.When == AfterEvent { - if cb.Event == e.Event { - cb.F(e) + if cb.Event == cc.Event { + cb.F(cc) } } if cb.When == AfterAllEvents { - cb.F(e) + cb.F(cc) } } } From cd3c5e79bd5d2f3983f5001cd5ee5fe319dfb01c Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 11 Jul 2022 08:49:24 +0200 Subject: [PATCH 23/33] Back to one alloc --- errors.go | 16 ++++++++++------ errors_test.go | 2 +- fsm.go | 6 ++++-- fsm_test.go | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/errors.go b/errors.go index 011bda3..fe14e6e 100644 --- a/errors.go +++ b/errors.go @@ -14,17 +14,21 @@ package fsm -import "fmt" +import ( + "fmt" + + "golang.org/x/exp/constraints" +) // InvalidEventError is returned by FSM.Event() when the event cannot be called // in the current state. -type InvalidEventError struct { - Event string - State string +type InvalidEventError[E constraints.Ordered, S constraints.Ordered] struct { + Event E + State S } -func (e InvalidEventError) Error() string { - return fmt.Sprintf("event %s inappropriate in current state %s", e.Event, e.State) +func (e InvalidEventError[E, S]) Error() string { + return fmt.Sprintf("event %v inappropriate in current state %v", e.Event, e.State) } // UnknownEventError is returned by FSM.Event() when the event is not defined. diff --git a/errors_test.go b/errors_test.go index ba384ee..e53259c 100644 --- a/errors_test.go +++ b/errors_test.go @@ -22,7 +22,7 @@ import ( func TestInvalidEventError(t *testing.T) { event := "invalid event" state := "state" - e := InvalidEventError{Event: event, State: state} + e := InvalidEventError[string, string]{Event: event, State: state} if e.Error() != "event "+e.Event+" inappropriate in current state "+e.State { t.Error("InvalidEventError string mismatch") } diff --git a/fsm.go b/fsm.go index 063160a..af9155b 100644 --- a/fsm.go +++ b/fsm.go @@ -25,6 +25,7 @@ package fsm import ( + "errors" "fmt" "sync" @@ -212,7 +213,7 @@ func (f *FSM[E, S]) Event(event E, args ...any) error { if !ok { for ekey := range f.transitions { if ekey.event == event { - return InvalidEventError{fmt.Sprintf("%v", event), fmt.Sprintf("%v", f.current)} + return InvalidEventError[E, S]{event, f.current} } } return UnknownEventError{fmt.Sprintf("%v", event)} @@ -241,7 +242,8 @@ func (f *FSM[E, S]) Event(event E, args ...any) error { } if err = f.leaveStateCallbacks(e); err != nil { - if _, ok := err.(CanceledError); ok { + var ce *CanceledError + if errors.As(err, &ce) { f.transition = nil } return err diff --git a/fsm_test.go b/fsm_test.go index 3647b3b..c7b3d13 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -88,7 +88,7 @@ func TestInappropriateEvent(t *testing.T) { Callbacks[string, string]{}, ) err := fsm.Event("close") - if e, ok := err.(InvalidEventError); !ok && e.Event != "close" && e.State != "closed" { + if e, ok := err.(InvalidEventError[string, string]); !ok && e.Event != "close" && e.State != "closed" { t.Error("expected 'InvalidEventError' with correct state and event") } } From cf20b0afbd85f4ad5f7a37562bfe08d7ecc45e48 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 11 Jul 2022 09:21:57 +0200 Subject: [PATCH 24/33] test metadata --- fsm_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/fsm_test.go b/fsm_test.go index c7b3d13..ec2661b 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -633,6 +633,33 @@ func TestCallbackArgs(t *testing.T) { } } +func TestCallbackMeta(t *testing.T) { + fsm := New( + "start", + Transitions[string, string]{ + {Event: "run", Src: []string{"start"}, Dst: "end"}, + }, + Callbacks[string, string]{ + Callback[string, string]{When: BeforeEvent, Event: "run", + F: func(e *CallbackContext[string, string]) { + value, ok := e.FSM.Metadata("key") + if !ok { + t.Error("no metadata with `key` found") + } + if value != "value" { + t.Error("incorrect value") + } + }, + }, + }, + ) + fsm.SetMetadata("key", "value") + err := fsm.Event("run") + if err != nil { + t.Errorf("transition failed %v", err) + } +} + func TestCallbackPanic(t *testing.T) { panicMsg := "unexpected panic" defer func() { From b213cec335cacb670b2e58560a1019366c84cebd Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 11 Jul 2022 09:34:09 +0200 Subject: [PATCH 25/33] godoc --- callback.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/callback.go b/callback.go index 026c33f..c81753c 100644 --- a/callback.go +++ b/callback.go @@ -40,6 +40,10 @@ const ( LeaveAllStates ) +// Callback defines a condition when the callback function F should be called in certain conditions. +// The order of execution for CallbackTypes in the same event or state is: +// The concrete CallbackType has precedence over a general one, e.g. +// BeforEvent E will be fired before BeforeAllEvents. type Callback[E constraints.Ordered, S constraints.Ordered] struct { // When should the callback be called. When CallbackType @@ -51,32 +55,25 @@ type Callback[E constraints.Ordered, S constraints.Ordered] struct { F func(*CallbackContext[E, S]) } -// Callbacks is a shorthand for defining the callbacks in NewFSM. +// Callbacks is a shorthand for defining the callbacks in New. type Callbacks[E constraints.Ordered, S constraints.Ordered] []Callback[E, S] // CallbackContext is the info that get passed as a reference in the callbacks. type CallbackContext[E constraints.Ordered, S constraints.Ordered] struct { // FSM is an reference to the current FSM. FSM *FSM[E, S] - // Event is the event name. Event E - // Src is the state before the transition. Src S - // Dst is the state after the transition. Dst S - // Err is an optional error that can be returned from a callback. Err error - // Args is an optional list of arguments passed to the callback. Args []any - // canceled is an internal flag set if the transition is canceled. canceled bool - // async is an internal flag set if the transition should be asynchronous async bool } From 040175ddd176806a0845cfbb5a855a969adc42ce Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 11 Jul 2022 09:42:34 +0200 Subject: [PATCH 26/33] no implicit structs --- fsm.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/fsm.go b/fsm.go index af9155b..9465133 100644 --- a/fsm.go +++ b/fsm.go @@ -219,7 +219,16 @@ func (f *FSM[E, S]) Event(event E, args ...any) error { return UnknownEventError{fmt.Sprintf("%v", event)} } - e := &CallbackContext[E, S]{f, event, f.current, dst, nil, args, false, false} + e := &CallbackContext[E, S]{ + FSM: f, + Event: event, + Src: f.current, + Dst: dst, + Err: nil, + Args: args, + canceled: false, + async: false, + } err := f.beforeEventCallbacks(e) if err != nil { From 28f66666337c725f336d8d0d8d281f189ab38282 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 11 Jul 2022 10:02:33 +0200 Subject: [PATCH 27/33] Add one more benchmark --- fsm_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/fsm_test.go b/fsm_test.go index ec2661b..5b44325 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -1083,3 +1083,29 @@ func BenchmarkFSM(b *testing.B) { _ = fsm.Event("open") } } + +func BenchmarkGenericFSMManyEvents(b *testing.B) { + transitions := Transitions[int, int]{} + for i := 0; i < 1000; i++ { + transitions = append(transitions, Transition[int, int]{Event: i, Src: []int{i}, Dst: i + 1}) + } + callbacks := Callbacks[int, int]{} + for i := 0; i < 1000; i++ { + callbacks = append(callbacks, Callback[int, int]{ + When: BeforeEvent, + Event: i, + F: func(cc *CallbackContext[int, int]) { + fmt.Print(cc.Event) + }, + }) + } + + fsm := New( + 0, + transitions, + callbacks, + ) + for i := 0; i < b.N; i++ { + _ = fsm.Event(1) + } +} From bb1ee6b5192f1b1af926630a18188f513f245958 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 11 Jul 2022 10:09:00 +0200 Subject: [PATCH 28/33] Enable benchmarks in test --- Makefile | 2 +- fsm_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 214408a..fe0da54 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ default: services test .PHONY: test test: - CGO_ENABLED=1 go test ./... -v -race -coverprofile=coverage.out -covermode=atomic && go tool cover -func=coverage.out + CGO_ENABLED=1 go test -benchmem -bench=. -v ./... -race -coverprofile=coverage.out -covermode=atomic && go tool cover -func=coverage.out .PHONY: lint lint: diff --git a/fsm_test.go b/fsm_test.go index 5b44325..b2fd6df 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -1086,11 +1086,11 @@ func BenchmarkFSM(b *testing.B) { func BenchmarkGenericFSMManyEvents(b *testing.B) { transitions := Transitions[int, int]{} - for i := 0; i < 1000; i++ { + for i := 0; i < 100; i++ { transitions = append(transitions, Transition[int, int]{Event: i, Src: []int{i}, Dst: i + 1}) } callbacks := Callbacks[int, int]{} - for i := 0; i < 1000; i++ { + for i := 0; i < 100; i++ { callbacks = append(callbacks, Callback[int, int]{ When: BeforeEvent, Event: i, From 0b31398888721f6a519aa3789ddb70b5865163bd Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 11 Jul 2022 10:26:21 +0200 Subject: [PATCH 29/33] Run tests with make target --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f126a41..40d5ca4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: go-version: 1.18 - name: Test - run: go test -coverprofile=coverage.out ./... + run: make test - name: Convert coverage uses: jandelgado/gcov2lcov-action@v1.0.5 From 1633e53b511dcfb62a9e3fc85154e4dbe2b7ba2a Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 11 Jul 2022 12:58:52 +0200 Subject: [PATCH 30/33] Named callbacktypes with validation --- callback.go | 64 +++++++-- examples/alternate.go | 8 +- examples/data.go | 8 +- examples/generic.go | 11 +- examples/simple.go | 8 +- examples/struct.go | 7 +- fsm.go | 5 +- fsm_test.go | 273 ++++++++++++++++++++++++++---------- graphviz_visualizer_test.go | 6 +- mermaid_visualizer_test.go | 12 +- 10 files changed, 300 insertions(+), 102 deletions(-) diff --git a/callback.go b/callback.go index c81753c..c982333 100644 --- a/callback.go +++ b/callback.go @@ -15,29 +15,31 @@ package fsm import ( + "fmt" + "golang.org/x/exp/constraints" ) // CallbackType defines at which type of Event this callback should be called. -type CallbackType int +type CallbackType string const ( // BeforeEvent called before event E - BeforeEvent CallbackType = iota + BeforeEvent = CallbackType("before_event") // BeforeAllEvents called before all events - BeforeAllEvents + BeforeAllEvents = CallbackType("before_all_events") // AfterEvent called after event E - AfterEvent + AfterEvent = CallbackType("after_event") // AfterAllEvents called after all events - AfterAllEvents + AfterAllEvents = CallbackType("after_all_events") // EnterState called after entering state S - EnterState + EnterState = CallbackType("enter_state") // EnterAllStates called after entering all states - EnterAllStates + EnterAllStates = CallbackType("enter_all_states") // LeaveState is called before leaving state S. - LeaveState + LeaveState = CallbackType("leave_state") // LeaveAllStates is called before leaving all states. - LeaveAllStates + LeaveAllStates = CallbackType("leave_all_states") ) // Callback defines a condition when the callback function F should be called in certain conditions. @@ -97,3 +99,47 @@ func (ctx *CallbackContext[E, S]) Cancel(err ...error) { func (ctx *CallbackContext[E, S]) Async() { ctx.async = true } +func (cs Callbacks[E, S]) validate() error { + for i := range cs { + cb := cs[i] + err := cb.validate() + if err != nil { + return err + } + } + return nil +} + +func (c *Callback[E, S]) validate() error { + var ( + zeroEvent E + zeroState S + ) + switch c.When { + case BeforeEvent, AfterEvent: + if c.Event == zeroEvent { + return fmt.Errorf("%v given but no event", c.When) + } + if c.State != zeroState { + return fmt.Errorf("%v given but state %v specified", c.When, c.State) + } + case BeforeAllEvents, AfterAllEvents: + if c.Event != zeroEvent { + return fmt.Errorf("%v given with event %v", c.When, c.Event) + } + case EnterState, LeaveState: + if c.State == zeroState { + return fmt.Errorf("%v given but no state", c.When) + } + if c.Event != zeroEvent { + return fmt.Errorf("%v given but event %v specified", c.When, c.Event) + } + case EnterAllStates, LeaveAllStates: + if c.State != zeroState { + return fmt.Errorf("%v given with state %v", c.When, c.State) + } + default: + return fmt.Errorf("invalid callback:%v", c) + } + return nil +} diff --git a/examples/alternate.go b/examples/alternate.go index 1b7f5fd..674fb70 100644 --- a/examples/alternate.go +++ b/examples/alternate.go @@ -10,7 +10,7 @@ import ( ) func main() { - f := fsm.New( + f, err := fsm.New( "idle", fsm.Transistions[string, string]{ {Event: "scan", Src: []string{"idle"}, Dst: "scanning"}, @@ -42,10 +42,12 @@ func main() { }, }, ) - + if err != nil { + fmt.Println(err) + } fmt.Println(f.Current()) - err := f.Event("scan") + err = f.Event("scan") if err != nil { fmt.Println(err) } diff --git a/examples/data.go b/examples/data.go index baeb77c..b7fb791 100644 --- a/examples/data.go +++ b/examples/data.go @@ -10,7 +10,7 @@ import ( ) func main() { - fsm := fsm.New( + fsm, err := fsm.New( "idle", fsm.Transistions[string, string]{ {Event: "produce", Src: []string{"idle"}, Dst: "idle"}, @@ -33,10 +33,12 @@ func main() { }, }, ) - + if err != nil { + fmt.Println(err) + } fmt.Println(fsm.Current()) - err := fsm.Event("produce") + err = fsm.Event("produce") if err != nil { fmt.Println(err) } diff --git a/examples/generic.go b/examples/generic.go index c212aa5..759172f 100644 --- a/examples/generic.go +++ b/examples/generic.go @@ -22,7 +22,7 @@ const ( ) func main() { - fsm := fsm.New( + fsm, err := fsm.New( IsClosed, fsm.Transitions[MyEvent, MyState]{ {Event: Open, Src: []MyState{IsClosed}, Dst: IsOpen}, @@ -36,15 +36,20 @@ func main() { }, }, fsm.Callback[MyEvent, MyState]{ - When: fsm.AfterAllEvents, + When: fsm.BeforeEvent, + Event: Open, + F: func(cr *fsm.CallbackContext[MyEvent, MyState]) { fmt.Printf("callback after all: event:%s src:%s dst:%s\n", cr.Event, cr.Src, cr.Dst) }, }, }, ) + if err != nil { + fmt.Println(err) + } fmt.Println(fsm.Current()) - err := fsm.Event(Open) + err = fsm.Event(Open) if err != nil { fmt.Println(err) } diff --git a/examples/simple.go b/examples/simple.go index a587b09..217b086 100644 --- a/examples/simple.go +++ b/examples/simple.go @@ -10,7 +10,7 @@ import ( ) func main() { - fsm := fsm.New( + fsm, err := fsm.New( "closed", fsm.Transistions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -18,10 +18,12 @@ func main() { }, fsm.Callbacks[string, string]{}, ) - + if err != nil { + fmt.Println(err) + } fmt.Println(fsm.Current()) - err := fsm.Event("open") + err = fsm.Event("open") if err != nil { fmt.Println(err) } diff --git a/examples/struct.go b/examples/struct.go index aa3ed1b..a5dc808 100644 --- a/examples/struct.go +++ b/examples/struct.go @@ -19,7 +19,8 @@ func NewDoor(to string) *Door { To: to, } - d.FSM = fsm.New( + var err error + d.FSM, err = fsm.New( "closed", fsm.Transistions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -31,7 +32,9 @@ func NewDoor(to string) *Door { }, }, ) - + if err != nil { + fmt.Println(err) + } return d } diff --git a/fsm.go b/fsm.go index 9465133..1b19199 100644 --- a/fsm.go +++ b/fsm.go @@ -97,7 +97,7 @@ type Transitions[E constraints.Ordered, S constraints.Ordered] []Transition[E, S // and a slice of source states, the destination state and the callback function. // // Callbacks are added as a slice specified as Callbacks and called in the same order. -func New[E constraints.Ordered, S constraints.Ordered](initial S, transitions Transitions[E, S], callbacks Callbacks[E, S]) *FSM[E, S] { +func New[E constraints.Ordered, S constraints.Ordered](initial S, transitions Transitions[E, S], callbacks Callbacks[E, S]) (*FSM[E, S], error) { f := &FSM[E, S]{ current: initial, transitioner: &defaultTransitioner[E, S]{}, @@ -113,7 +113,8 @@ func New[E constraints.Ordered, S constraints.Ordered](initial S, transitions Tr f.transitions[eKey[E, S]{e.Event, src}] = e.Dst } } - return f + err := callbacks.validate() + return f, err } // Current returns the current state of the FSM. diff --git a/fsm_test.go b/fsm_test.go index b2fd6df..5970026 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -32,13 +32,16 @@ func (t fakeTransitioner[E, S]) transition(f *FSM[E, S]) error { } func TestSameState(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string, string]{}, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } _ = fsm.Event("run") if fsm.Current() != "start" { t.Error("expected state to be 'start'") @@ -46,40 +49,48 @@ func TestSameState(t *testing.T) { } func TestSetState(t *testing.T) { - fsm := New( + fsm, err := New( "walking", Transitions[string, string]{ {Event: "walk", Src: []string{"start"}, Dst: "walking"}, }, Callbacks[string, string]{}, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } + fsm.SetState("start") if fsm.Current() != "start" { t.Error("expected state to be 'walking'") } - err := fsm.Event("walk") + err = fsm.Event("walk") if err != nil { t.Error("transition is expected no error") } } func TestBadTransition(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "running"}, }, Callbacks[string, string]{}, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } + fsm.transitioner = new(fakeTransitioner[string, string]) - err := fsm.Event("run") + err = fsm.Event("run") if err == nil { t.Error("bad transition should give an error") } } func TestInappropriateEvent(t *testing.T) { - fsm := New( + fsm, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -87,14 +98,18 @@ func TestInappropriateEvent(t *testing.T) { }, Callbacks[string, string]{}, ) - err := fsm.Event("close") + if err != nil { + t.Errorf("constructor failed:%s", err) + } + + err = fsm.Event("close") if e, ok := err.(InvalidEventError[string, string]); !ok && e.Event != "close" && e.State != "closed" { t.Error("expected 'InvalidEventError' with correct state and event") } } func TestInvalidEvent(t *testing.T) { - fsm := New( + fsm, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -102,14 +117,18 @@ func TestInvalidEvent(t *testing.T) { }, Callbacks[string, string]{}, ) - err := fsm.Event("lock") + if err != nil { + t.Errorf("constructor failed:%s", err) + } + + err = fsm.Event("lock") if e, ok := err.(UnknownEventError); !ok && e.Event != "close" { t.Error("expected 'UnknownEventError' with correct event") } } func TestMultipleSources(t *testing.T) { - fsm := New( + fsm, err := New( "one", Transitions[string, string]{ {Event: "first", Src: []string{"one"}, Dst: "two"}, @@ -118,8 +137,11 @@ func TestMultipleSources(t *testing.T) { }, Callbacks[string, string]{}, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } - err := fsm.Event("first") + err = fsm.Event("first") if err != nil { t.Errorf("transition failed %v", err) } @@ -154,7 +176,7 @@ func TestMultipleSources(t *testing.T) { } func TestMultipleEvents(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "first", Src: []string{"start"}, Dst: "one"}, @@ -165,8 +187,11 @@ func TestMultipleEvents(t *testing.T) { }, Callbacks[string, string]{}, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } - err := fsm.Event("first") + err = fsm.Event("first") if err != nil { t.Errorf("transition failed %v", err) } @@ -211,7 +236,7 @@ func TestGenericCallbacks(t *testing.T) { enterState := false afterEvent := false - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -239,8 +264,11 @@ func TestGenericCallbacks(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } - err := fsm.Event("run") + err = fsm.Event("run") if err != nil { t.Errorf("transition failed %v", err) } @@ -255,7 +283,7 @@ func TestSpecificCallbacks(t *testing.T) { enterState := false afterEvent := false - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -283,8 +311,11 @@ func TestSpecificCallbacks(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } - err := fsm.Event("run") + err = fsm.Event("run") if err != nil { t.Errorf("transition failed %v", err) } @@ -297,7 +328,7 @@ func TestSpecificCallbacksShortform(t *testing.T) { enterState := false afterEvent := false - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -315,8 +346,11 @@ func TestSpecificCallbacksShortform(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } - err := fsm.Event("run") + err = fsm.Event("run") if err != nil { t.Errorf("transition failed %v", err) } @@ -328,7 +362,7 @@ func TestSpecificCallbacksShortform(t *testing.T) { func TestBeforeEventWithoutTransition(t *testing.T) { beforeEvent := true - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "dontrun", Src: []string{"start"}, Dst: "start"}, @@ -341,8 +375,11 @@ func TestBeforeEventWithoutTransition(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } - err := fsm.Event("dontrun") + err = fsm.Event("dontrun") if e, ok := err.(NoTransitionError); !ok && e.Err != nil { t.Error("expected 'NoTransitionError' without custom error") } @@ -356,7 +393,7 @@ func TestBeforeEventWithoutTransition(t *testing.T) { } func TestCancelBeforeGenericEvent(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -369,6 +406,9 @@ func TestCancelBeforeGenericEvent(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } _ = fsm.Event("run") if fsm.Current() != "start" { t.Error("expected state to be 'start'") @@ -376,7 +416,7 @@ func TestCancelBeforeGenericEvent(t *testing.T) { } func TestCancelBeforeSpecificEvent(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -389,6 +429,9 @@ func TestCancelBeforeSpecificEvent(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } _ = fsm.Event("run") if fsm.Current() != "start" { t.Error("expected state to be 'start'") @@ -396,7 +439,7 @@ func TestCancelBeforeSpecificEvent(t *testing.T) { } func TestCancelLeaveGenericState(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -409,6 +452,10 @@ func TestCancelLeaveGenericState(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } + _ = fsm.Event("run") if fsm.Current() != "start" { t.Error("expected state to be 'start'") @@ -416,7 +463,7 @@ func TestCancelLeaveGenericState(t *testing.T) { } func TestCancelLeaveSpecificState(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -429,6 +476,10 @@ func TestCancelLeaveSpecificState(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } + _ = fsm.Event("run") if fsm.Current() != "start" { t.Error("expected state to be 'start'") @@ -436,7 +487,7 @@ func TestCancelLeaveSpecificState(t *testing.T) { } func TestCancelWithError(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -449,7 +500,10 @@ func TestCancelWithError(t *testing.T) { }, }, ) - err := fsm.Event("run") + if err != nil { + t.Errorf("constructor failed:%s", err) + } + err = fsm.Event("run") if _, ok := err.(CanceledError); !ok { t.Error("expected only 'CanceledError'") } @@ -464,7 +518,7 @@ func TestCancelWithError(t *testing.T) { } func TestAsyncTransitionGenericState(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -477,11 +531,14 @@ func TestAsyncTransitionGenericState(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } _ = fsm.Event("run") if fsm.Current() != "start" { t.Error("expected state to be 'start'") } - err := fsm.Transition() + err = fsm.Transition() if err != nil { t.Errorf("transition failed %v", err) } @@ -491,7 +548,7 @@ func TestAsyncTransitionGenericState(t *testing.T) { } func TestAsyncTransitionSpecificState(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -504,11 +561,14 @@ func TestAsyncTransitionSpecificState(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } _ = fsm.Event("run") if fsm.Current() != "start" { t.Error("expected state to be 'start'") } - err := fsm.Transition() + err = fsm.Transition() if err != nil { t.Errorf("transition failed %v", err) } @@ -518,7 +578,7 @@ func TestAsyncTransitionSpecificState(t *testing.T) { } func TestAsyncTransitionInProgress(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -532,8 +592,11 @@ func TestAsyncTransitionInProgress(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } _ = fsm.Event("run") - err := fsm.Event("reset") + err = fsm.Event("reset") if e, ok := err.(InTransitionError); !ok && e.Event != "reset" { t.Error("expected 'InTransitionError' with correct state") } @@ -551,7 +614,7 @@ func TestAsyncTransitionInProgress(t *testing.T) { } func TestAsyncTransitionNotInProgress(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -559,14 +622,17 @@ func TestAsyncTransitionNotInProgress(t *testing.T) { }, Callbacks[string, string]{}, ) - err := fsm.Transition() + if err != nil { + t.Errorf("constructor failed:%s", err) + } + err = fsm.Transition() if _, ok := err.(NotInTransitionError); !ok { t.Error("expected 'NotInTransitionError'") } } func TestCallbackNoError(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -578,6 +644,9 @@ func TestCallbackNoError(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } e := fsm.Event("run") if e != nil { t.Error("expected no error") @@ -585,7 +654,7 @@ func TestCallbackNoError(t *testing.T) { } func TestCallbackError(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -598,6 +667,9 @@ func TestCallbackError(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } e := fsm.Event("run") if e.Error() != "error" { t.Error("expected error to be 'error'") @@ -605,7 +677,7 @@ func TestCallbackError(t *testing.T) { } func TestCallbackArgs(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -627,14 +699,17 @@ func TestCallbackArgs(t *testing.T) { }, }, ) - err := fsm.Event("run", "test") + if err != nil { + t.Errorf("constructor failed:%s", err) + } + err = fsm.Event("run", "test") if err != nil { t.Errorf("transition failed %v", err) } } func TestCallbackMeta(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -653,8 +728,11 @@ func TestCallbackMeta(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } fsm.SetMetadata("key", "value") - err := fsm.Event("run") + err = fsm.Event("run") if err != nil { t.Errorf("transition failed %v", err) } @@ -668,7 +746,7 @@ func TestCallbackPanic(t *testing.T) { t.Errorf("expected panic message to be '%s', got %v", panicMsg, r) } }() - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -681,6 +759,9 @@ func TestCallbackPanic(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } e := fsm.Event("run") if e.Error() != "error" { t.Error("expected error to be 'error'") @@ -689,7 +770,7 @@ func TestCallbackPanic(t *testing.T) { func TestNoDeadLock(t *testing.T) { var fsm *FSM[string, string] - fsm = New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -702,14 +783,17 @@ func TestNoDeadLock(t *testing.T) { }, }, ) - err := fsm.Event("run") + if err != nil { + t.Errorf("constructor failed:%s", err) + } + err = fsm.Event("run") if err != nil { t.Errorf("transition failed %v", err) } } func TestThreadSafetyRaceCondition(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -721,13 +805,16 @@ func TestThreadSafetyRaceCondition(t *testing.T) { }, }, ) + if err != nil { + t.Errorf("constructor failed:%s", err) + } var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() _ = fsm.Current() }() - err := fsm.Event("run") + err = fsm.Event("run") if err != nil { t.Errorf("transition failed %v", err) } @@ -738,7 +825,7 @@ func TestDoubleTransition(t *testing.T) { var fsm *FSM[string, string] var wg sync.WaitGroup wg.Add(2) - fsm = New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "end"}, @@ -769,28 +856,34 @@ func TestDoubleTransition(t *testing.T) { }, }, ) - if err := fsm.Event("run"); err != nil { + if err != nil { + t.Errorf("constructor failed:%s", err) + } + if err = fsm.Event("run"); err != nil { fmt.Println(err) } wg.Wait() } func TestNoTransition(t *testing.T) { - fsm := New( + fsm, err := New( "start", Transitions[string, string]{ {Event: "run", Src: []string{"start"}, Dst: "start"}, }, Callbacks[string, string]{}, ) - err := fsm.Event("run") + if err != nil { + t.Errorf("constructor failed:%s", err) + } + err = fsm.Event("run") if _, ok := err.(NoTransitionError); !ok { t.Error("expected 'NoTransitionError'") } } func ExampleNew() { - fsm := New( + fsm, err := New( "green", Transitions[string, string]{ {Event: "warn", Src: []string{"green"}, Dst: "yellow"}, @@ -842,8 +935,11 @@ func ExampleNew() { }, }, ) + if err != nil { + fmt.Println(err) + } fmt.Println(fsm.Current()) - err := fsm.Event("warn") + err = fsm.Event("warn") if err != nil { fmt.Println(err) } @@ -862,7 +958,7 @@ func ExampleNew() { } func ExampleFSM_Current() { - fsm := New( + fsm, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -870,12 +966,15 @@ func ExampleFSM_Current() { }, Callbacks[string, string]{}, ) + if err != nil { + fmt.Println(err) + } fmt.Println(fsm.Current()) // Output: closed } func ExampleFSM_Is() { - fsm := New( + fsm, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -883,6 +982,9 @@ func ExampleFSM_Is() { }, Callbacks[string, string]{}, ) + if err != nil { + fmt.Println(err) + } fmt.Println(fsm.Is("closed")) fmt.Println(fsm.Is("open")) // Output: @@ -891,7 +993,7 @@ func ExampleFSM_Is() { } func ExampleFSM_Can() { - fsm := New( + fsm, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -899,6 +1001,9 @@ func ExampleFSM_Can() { }, Callbacks[string, string]{}, ) + if err != nil { + fmt.Println(err) + } fmt.Println(fsm.Can("open")) fmt.Println(fsm.Can("close")) // Output: @@ -907,7 +1012,7 @@ func ExampleFSM_Can() { } func ExampleFSM_AvailableTransitions() { - fsm := New( + fsm, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -916,6 +1021,9 @@ func ExampleFSM_AvailableTransitions() { }, Callbacks[string, string]{}, ) + if err != nil { + fmt.Println(err) + } // sort the results ordering is consistent for the output checker transitions := fsm.AvailableTransitions() sort.Strings(transitions) @@ -925,7 +1033,7 @@ func ExampleFSM_AvailableTransitions() { } func ExampleFSM_Cannot() { - fsm := New( + fsm, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -933,6 +1041,9 @@ func ExampleFSM_Cannot() { }, Callbacks[string, string]{}, ) + if err != nil { + fmt.Println(err) + } fmt.Println(fsm.Cannot("open")) fmt.Println(fsm.Cannot("close")) // Output: @@ -941,7 +1052,7 @@ func ExampleFSM_Cannot() { } func ExampleFSM_Event() { - fsm := New( + fsm, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -949,8 +1060,11 @@ func ExampleFSM_Event() { }, Callbacks[string, string]{}, ) + if err != nil { + fmt.Println(err) + } fmt.Println(fsm.Current()) - err := fsm.Event("open") + err = fsm.Event("open") if err != nil { fmt.Println(err) } @@ -967,7 +1081,7 @@ func ExampleFSM_Event() { } func ExampleFSM_Transition() { - fsm := New( + fsm, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -982,7 +1096,10 @@ func ExampleFSM_Transition() { }, }, ) - err := fsm.Event("open") + if err != nil { + fmt.Println(err) + } + err = fsm.Event("open") if e, ok := err.(AsyncError); !ok && e.Err != nil { fmt.Println(err) } @@ -1010,7 +1127,7 @@ const ( ) func ExampleFSM_Event_generic() { - fsm := New( + fsm, err := New( IsClosed, Transitions[MyEvent, MyState]{ {Event: Open, Src: []MyState{IsClosed}, Dst: IsOpen}, @@ -1018,15 +1135,19 @@ func ExampleFSM_Event_generic() { }, Callbacks[MyEvent, MyState]{ Callback[MyEvent, MyState]{ - When: BeforeEvent, + When: BeforeEvent, + Event: Close, F: func(cc *CallbackContext[MyEvent, MyState]) { }, }, }, ) + if err != nil { + fmt.Println(err) + } fmt.Println(fsm.Current()) - err := fsm.Event(Open) + err = fsm.Event(Open) if err != nil { fmt.Println(err) } @@ -1043,7 +1164,7 @@ func ExampleFSM_Event_generic() { } func BenchmarkGenericFSM(b *testing.B) { - fsm := New( + fsm, err := New( IsClosed, Transitions[MyEvent, MyState]{ {Event: Open, Src: []MyState{IsClosed}, Dst: IsOpen}, @@ -1052,19 +1173,23 @@ func BenchmarkGenericFSM(b *testing.B) { Callbacks[MyEvent, MyState]{ Callback[MyEvent, MyState]{ - When: BeforeEvent, + When: BeforeEvent, + Event: Open, F: func(cc *CallbackContext[MyEvent, MyState]) { }, }, }, ) + if err != nil { + fmt.Println(err) + } for i := 0; i < b.N; i++ { _ = fsm.Event(Open) } } func BenchmarkFSM(b *testing.B) { - fsm := New( + fsm, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -1072,13 +1197,17 @@ func BenchmarkFSM(b *testing.B) { }, Callbacks[string, string]{ Callback[string, string]{ - When: BeforeEvent, + When: BeforeEvent, + Event: "open", F: func(cc *CallbackContext[string, string]) { }, }, }, ) + if err != nil { + fmt.Println(err) + } for i := 0; i < b.N; i++ { _ = fsm.Event("open") } @@ -1092,19 +1221,21 @@ func BenchmarkGenericFSMManyEvents(b *testing.B) { callbacks := Callbacks[int, int]{} for i := 0; i < 100; i++ { callbacks = append(callbacks, Callback[int, int]{ - When: BeforeEvent, - Event: i, + When: BeforeAllEvents, F: func(cc *CallbackContext[int, int]) { fmt.Print(cc.Event) }, }) } - fsm := New( + fsm, err := New( 0, transitions, callbacks, ) + if err != nil { + fmt.Println(err) + } for i := 0; i < b.N; i++ { _ = fsm.Event(1) } diff --git a/graphviz_visualizer_test.go b/graphviz_visualizer_test.go index ccb3b70..b1f8fac 100644 --- a/graphviz_visualizer_test.go +++ b/graphviz_visualizer_test.go @@ -7,7 +7,7 @@ import ( ) func TestGraphvizOutput(t *testing.T) { - fsmUnderTest := New( + fsmUnderTest, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -16,7 +16,9 @@ func TestGraphvizOutput(t *testing.T) { }, Callbacks[string, string]{}, ) - + if err != nil { + t.Errorf("constructor failed:%s", err) + } got := Visualize(fsmUnderTest) wanted := ` digraph fsm { diff --git a/mermaid_visualizer_test.go b/mermaid_visualizer_test.go index c93a439..ee67a5d 100644 --- a/mermaid_visualizer_test.go +++ b/mermaid_visualizer_test.go @@ -7,7 +7,7 @@ import ( ) func TestMermaidOutput(t *testing.T) { - fsmUnderTest := New( + fsmUnderTest, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -16,7 +16,9 @@ func TestMermaidOutput(t *testing.T) { }, Callbacks[string, string]{}, ) - + if err != nil { + t.Errorf("constructor failed:%s", err) + } got, err := VisualizeForMermaidWithGraphType(fsmUnderTest, StateDiagram) if err != nil { t.Errorf("got error for visualizing with type MERMAID: %s", err) @@ -38,7 +40,7 @@ stateDiagram-v2 } func TestMermaidFlowChartOutput(t *testing.T) { - fsmUnderTest := New( + fsmUnderTest, err := New( "closed", Transitions[string, string]{ {Event: "open", Src: []string{"closed"}, Dst: "open"}, @@ -49,7 +51,9 @@ func TestMermaidFlowChartOutput(t *testing.T) { }, Callbacks[string, string]{}, ) - + if err != nil { + t.Errorf("constructor failed:%s", err) + } got, err := VisualizeForMermaidWithGraphType(fsmUnderTest, FlowChart) if err != nil { t.Errorf("got error for visualizing with type MERMAID: %s", err) From 54758d459c7f800fc3eb0f4895309911321d4457 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 11 Jul 2022 13:05:17 +0200 Subject: [PATCH 31/33] Even more unsupported callbacks --- callback.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/callback.go b/callback.go index c982333..8bf607b 100644 --- a/callback.go +++ b/callback.go @@ -127,6 +127,9 @@ func (c *Callback[E, S]) validate() error { if c.Event != zeroEvent { return fmt.Errorf("%v given with event %v", c.When, c.Event) } + if c.State != zeroState { + return fmt.Errorf("%v given with state %v", c.When, c.State) + } case EnterState, LeaveState: if c.State == zeroState { return fmt.Errorf("%v given but no state", c.When) @@ -138,6 +141,9 @@ func (c *Callback[E, S]) validate() error { if c.State != zeroState { return fmt.Errorf("%v given with state %v", c.When, c.State) } + if c.Event != zeroEvent { + return fmt.Errorf("%v given with event %v", c.When, c.Event) + } default: return fmt.Errorf("invalid callback:%v", c) } From fe6eccbb0daff89dd20600c10ef319213e775cd2 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 11 Jul 2022 15:00:41 +0200 Subject: [PATCH 32/33] Add missing file --- callback_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 callback_test.go diff --git a/callback_test.go b/callback_test.go new file mode 100644 index 0000000..8f040ac --- /dev/null +++ b/callback_test.go @@ -0,0 +1,62 @@ +package fsm + +import "testing" + +func TestCallbackValidate(t *testing.T) { + tests := []struct { + name string + cb Callback[string, string] + errString string + }{ + { + name: "before_event without event", + cb: Callback[string, string]{When: BeforeEvent}, + errString: "before_event given but no event", + }, + { + name: "before_event with state", + cb: Callback[string, string]{When: BeforeEvent, Event: "open", State: "closed"}, + errString: "before_event given but state closed specified", + }, + { + name: "before_event with state", + cb: Callback[string, string]{When: BeforeAllEvents, Event: "open"}, + errString: "before_all_events given with event open", + }, + + { + name: "before_event without event", + cb: Callback[string, string]{When: EnterState}, + errString: "enter_state given but no state", + }, + { + name: "before_event with state", + cb: Callback[string, string]{When: EnterState, Event: "open", State: "closed"}, + errString: "enter_state given but event open specified", + }, + { + name: "before_event with state", + cb: Callback[string, string]{When: EnterAllStates, State: "closed"}, + errString: "enter_all_states given with state closed", + }, + } + + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + err := tt.cb.validate() + + if tt.errString == "" && err != nil { + t.Errorf("err:%v", err) + } + if tt.errString != "" && err == nil { + t.Errorf("errstring:%s but err is nil", tt.errString) + } + + if tt.errString != "" && err.Error() != tt.errString { + t.Errorf("transition failed %v", err) + } + }) + } + +} From 2925185a8877453e733e6accb3d8bfca38795811 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 13 Jul 2022 07:36:51 +0200 Subject: [PATCH 33/33] V2 --- README.md | 4 ++-- examples/alternate.go | 2 +- examples/data.go | 2 +- examples/generic.go | 2 +- examples/simple.go | 2 +- examples/struct.go | 2 +- go.mod | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1582966..14476cf 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ package main import ( "fmt" - "github.com/looplab/fsm" + "github.com/looplab/fsm/v2" ) func main() { @@ -64,7 +64,7 @@ package main import ( "fmt" - "github.com/looplab/fsm" + "github.com/looplab/fsm/v2" ) type Door struct { diff --git a/examples/alternate.go b/examples/alternate.go index 674fb70..c82d27f 100644 --- a/examples/alternate.go +++ b/examples/alternate.go @@ -6,7 +6,7 @@ package main import ( "fmt" - "github.com/looplab/fsm" + "github.com/looplab/fsm/v2" ) func main() { diff --git a/examples/data.go b/examples/data.go index b7fb791..20f95ee 100644 --- a/examples/data.go +++ b/examples/data.go @@ -6,7 +6,7 @@ package main import ( "fmt" - "github.com/looplab/fsm" + "github.com/looplab/fsm/v2" ) func main() { diff --git a/examples/generic.go b/examples/generic.go index 759172f..a1f6c2f 100644 --- a/examples/generic.go +++ b/examples/generic.go @@ -6,7 +6,7 @@ package main import ( "fmt" - "github.com/looplab/fsm" + "github.com/looplab/fsm/v2" ) type MyEvent string diff --git a/examples/simple.go b/examples/simple.go index 217b086..947448e 100644 --- a/examples/simple.go +++ b/examples/simple.go @@ -6,7 +6,7 @@ package main import ( "fmt" - "github.com/looplab/fsm" + "github.com/looplab/fsm/v2" ) func main() { diff --git a/examples/struct.go b/examples/struct.go index a5dc808..8131277 100644 --- a/examples/struct.go +++ b/examples/struct.go @@ -6,7 +6,7 @@ package main import ( "fmt" - "github.com/looplab/fsm" + "github.com/looplab/fsm/v2" ) type Door struct { diff --git a/go.mod b/go.mod index 623113b..408ec1a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/looplab/fsm +module github.com/looplab/fsm/v2 go 1.18