Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,48 @@ func main() {
}
```

# Shared machine definitions

Applications that keep one FSM per managed object can compile the events and
callbacks once into a `Spec` and share it between all machines, instead of
building the same transition and callback maps for every machine:

```go
package main

import (
"context"
"fmt"

"github.com/looplab/fsm"
)

var doorSpec = fsm.NewSpec(
fsm.Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
fsm.Callbacks{
"enter_state": func(_ context.Context, e *fsm.Event) {
fmt.Println("the door is", e.Dst)
},
},
)

func main() {
for i := 0; i < 3; i++ {
door := fsm.NewFSMFromSpec("closed", doorSpec)
if err := door.Event(context.Background(), "open"); err != nil {
fmt.Println(err)
}
}
}
```

A `Spec` is immutable and holds no state of its own, so any number of machines
can use it, also concurrently. Everything that changes while a machine runs is
kept per machine.

# License

FSM is licensed under Apache License 2.0
Expand Down
117 changes: 25 additions & 92 deletions fsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ package fsm

import (
"context"
"strings"
"sync"
)

Expand All @@ -36,16 +35,16 @@ type transitioner interface {

// FSM is the state machine that holds the current state.
//
// It has to be created with NewFSM to function properly.
// It has to be created with NewFSM, or with NewFSMFromSpec when the compiled
// machine description is shared with other FSMs, to function properly. The
// zero value has no transitions and rejects every event.
type FSM 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

// callbacks maps events and targets to callback functions.
callbacks map[cKey]Callback
// spec holds the compiled transitions and callbacks, it may be shared
// with other FSMs.
spec *Spec

// transition is the internal transition functions used either directly
// or when Transition is called in an asynchronous state transition.
Expand Down Expand Up @@ -128,80 +127,11 @@ 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.
//
// The events and callbacks are compiled into a Spec, use NewSpec and
// NewFSMFromSpec directly to share one compiled description between many FSMs.
func NewFSM(initial string, events []EventDesc, callbacks map[string]Callback) *FSM {
f := &FSM{
transitionerObj: &transitionerStruct{},
current: initial,
transitions: make(map[eKey]string),
callbacks: make(map[cKey]Callback),
metadata: make(map[string]interface{}),
}

// Build transition map and store sets of all events and states.
allEvents := make(map[string]bool)
allStates := make(map[string]bool)
for _, e := range events {
for _, src := range e.Src {
f.transitions[eKey{e.Name, src}] = e.Dst
allStates[src] = true
allStates[e.Dst] = true
}
allEvents[e.Name] = true
}

// Map all callbacks to events/states.
for name, fn := range callbacks {
var target string
var callbackType int

switch {
case strings.HasPrefix(name, "before_"):
target = strings.TrimPrefix(name, "before_")
if target == "event" {
target = ""
callbackType = callbackBeforeEvent
} else if _, ok := allEvents[target]; ok {
callbackType = callbackBeforeEvent
}
case strings.HasPrefix(name, "leave_"):
target = strings.TrimPrefix(name, "leave_")
if target == "state" {
target = ""
callbackType = callbackLeaveState
} else if _, ok := allStates[target]; ok {
callbackType = callbackLeaveState
}
case strings.HasPrefix(name, "enter_"):
target = strings.TrimPrefix(name, "enter_")
if target == "state" {
target = ""
callbackType = callbackEnterState
} else if _, ok := allStates[target]; ok {
callbackType = callbackEnterState
}
case strings.HasPrefix(name, "after_"):
target = strings.TrimPrefix(name, "after_")
if target == "event" {
target = ""
callbackType = callbackAfterEvent
} else if _, ok := allEvents[target]; ok {
callbackType = callbackAfterEvent
}
default:
target = name
if _, ok := allStates[target]; ok {
callbackType = callbackEnterState
} else if _, ok := allEvents[target]; ok {
callbackType = callbackAfterEvent
}
}

if callbackType != callbackNone {
f.callbacks[cKey{target, callbackType}] = fn
}
}

return f
return NewFSMFromSpec(initial, NewSpec(events, callbacks))
}

// Current returns the current state of the FSM.
Expand Down Expand Up @@ -232,7 +162,7 @@ func (f *FSM) Can(event string) bool {
defer f.eventMu.Unlock()
f.stateMu.RLock()
defer f.stateMu.RUnlock()
_, ok := f.transitions[eKey{event, f.current}]
_, ok := f.spec.transitionFor(event, f.current)
return ok && (f.transition == nil)
}

Expand All @@ -242,7 +172,7 @@ func (f *FSM) AvailableTransitions() []string {
f.stateMu.RLock()
defer f.stateMu.RUnlock()
var transitions []string
for key := range f.transitions {
for key := range f.spec.transitionTable() {
if key.src == f.current {
transitions = append(transitions, key.event)
}
Expand All @@ -268,6 +198,9 @@ func (f *FSM) Metadata(key string) (interface{}, bool) {
func (f *FSM) SetMetadata(key string, dataValue interface{}) {
f.metadataMu.Lock()
defer f.metadataMu.Unlock()
if f.metadata == nil {
f.metadata = make(map[string]interface{})
}
f.metadata[key] = dataValue
}

Expand Down Expand Up @@ -315,9 +248,9 @@ func (f *FSM) Event(ctx context.Context, event string, args ...interface{}) erro
return InTransitionError{event}
}

dst, ok := f.transitions[eKey{event, f.current}]
dst, ok := f.spec.transitionFor(event, f.current)
if !ok {
for ekey := range f.transitions {
for ekey := range f.spec.transitionTable() {
if ekey.event == event {
return InvalidEventError{event, f.current}
}
Expand Down Expand Up @@ -432,13 +365,13 @@ func (t transitionerStruct) transition(f *FSM) error {
// beforeEventCallbacks calls the before_ callbacks, first the named then the
// general version.
func (f *FSM) beforeEventCallbacks(ctx context.Context, e *Event) error {
if fn, ok := f.callbacks[cKey{e.Event, callbackBeforeEvent}]; ok {
if fn, ok := f.spec.callbackFor(e.Event, callbackBeforeEvent); ok {
fn(ctx, e)
if e.canceled {
return CanceledError{e.Err}
}
}
if fn, ok := f.callbacks[cKey{"", callbackBeforeEvent}]; ok {
if fn, ok := f.spec.callbackFor("", callbackBeforeEvent); ok {
fn(ctx, e)
if e.canceled {
return CanceledError{e.Err}
Expand All @@ -450,15 +383,15 @@ func (f *FSM) beforeEventCallbacks(ctx context.Context, e *Event) error {
// leaveStateCallbacks calls the leave_ callbacks, first the named then the
// general version.
func (f *FSM) leaveStateCallbacks(ctx context.Context, e *Event) error {
if fn, ok := f.callbacks[cKey{f.current, callbackLeaveState}]; ok {
if fn, ok := f.spec.callbackFor(f.current, callbackLeaveState); ok {
fn(ctx, e)
if e.canceled {
return CanceledError{e.Err}
} else if e.async {
return AsyncError{Err: e.Err}
}
}
if fn, ok := f.callbacks[cKey{"", callbackLeaveState}]; ok {
if fn, ok := f.spec.callbackFor("", callbackLeaveState); ok {
fn(ctx, e)
if e.canceled {
return CanceledError{e.Err}
Expand All @@ -472,21 +405,21 @@ func (f *FSM) leaveStateCallbacks(ctx context.Context, e *Event) error {
// enterStateCallbacks calls the enter_ callbacks, first the named then the
// general version.
func (f *FSM) enterStateCallbacks(ctx context.Context, e *Event) {
if fn, ok := f.callbacks[cKey{f.current, callbackEnterState}]; ok {
if fn, ok := f.spec.callbackFor(f.current, callbackEnterState); ok {
fn(ctx, e)
}
if fn, ok := f.callbacks[cKey{"", callbackEnterState}]; ok {
if fn, ok := f.spec.callbackFor("", callbackEnterState); ok {
fn(ctx, e)
}
}

// afterEventCallbacks calls the after_ callbacks, first the named then the
// general version.
func (f *FSM) afterEventCallbacks(ctx context.Context, e *Event) {
if fn, ok := f.callbacks[cKey{e.Event, callbackAfterEvent}]; ok {
if fn, ok := f.spec.callbackFor(e.Event, callbackAfterEvent); ok {
fn(ctx, e)
}
if fn, ok := f.callbacks[cKey{"", callbackAfterEvent}]; ok {
if fn, ok := f.spec.callbackFor("", callbackAfterEvent); ok {
fn(ctx, e)
}
}
Expand Down
11 changes: 7 additions & 4 deletions graphviz_visualizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import (
func Visualize(fsm *FSM) string {
var buf bytes.Buffer

transitions := fsm.spec.transitionTable()
current := fsm.Current()

// we sort the key alphabetically to have a reproducible graph output
sortedEKeys := getSortedTransitionKeys(fsm.transitions)
sortedStateKeys, _ := getSortedStates(fsm.transitions)
sortedEKeys := getSortedTransitionKeys(transitions)
sortedStateKeys, _ := getSortedStates(transitions)

writeHeaderLine(&buf)
writeTransitions(&buf, sortedEKeys, fsm.transitions)
writeStates(&buf, fsm.current, sortedStateKeys)
writeTransitions(&buf, sortedEKeys, transitions)
writeStates(&buf, current, sortedStateKeys)
writeFooter(&buf)

return buf.String()
Expand Down
17 changes: 10 additions & 7 deletions mermaid_visualizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ func VisualizeForMermaidWithGraphType(fsm *FSM, graphType MermaidDiagramType) (s
func visualizeForMermaidAsStateDiagram(fsm *FSM) string {
var buf bytes.Buffer

sortedTransitionKeys := getSortedTransitionKeys(fsm.transitions)
transitions := fsm.spec.transitionTable()
sortedTransitionKeys := getSortedTransitionKeys(transitions)

buf.WriteString("stateDiagram-v2\n")
buf.WriteString(fmt.Sprintln(` [*] -->`, fsm.current))
buf.WriteString(fmt.Sprintln(` [*] -->`, fsm.Current()))

for _, k := range sortedTransitionKeys {
v := fsm.transitions[k]
v := transitions[k]
buf.WriteString(fmt.Sprintf(` %s --> %s: %s`, k.src, v, k.event))
buf.WriteString("\n")
}
Expand All @@ -50,13 +51,15 @@ func visualizeForMermaidAsStateDiagram(fsm *FSM) string {
func visualizeForMermaidAsFlowChart(fsm *FSM) string {
var buf bytes.Buffer

sortedTransitionKeys := getSortedTransitionKeys(fsm.transitions)
sortedStates, statesToIDMap := getSortedStates(fsm.transitions)
transitions := fsm.spec.transitionTable()
current := fsm.Current()
sortedTransitionKeys := getSortedTransitionKeys(transitions)
sortedStates, statesToIDMap := getSortedStates(transitions)

writeFlowChartGraphType(&buf)
writeFlowChartStates(&buf, sortedStates, statesToIDMap)
writeFlowChartTransitions(&buf, fsm.transitions, sortedTransitionKeys, statesToIDMap)
writeFlowChartHighlightCurrent(&buf, fsm.current, statesToIDMap)
writeFlowChartTransitions(&buf, transitions, sortedTransitionKeys, statesToIDMap)
writeFlowChartHighlightCurrent(&buf, current, statesToIDMap)

return buf.String()
}
Expand Down
Loading
Loading