109 lines
2.9 KiB
Go
109 lines
2.9 KiB
Go
|
package statemachine
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"sync"
|
||
|
)
|
||
|
|
||
|
// This code has been shamelessly lifted from this blog post:
|
||
|
// https://venilnoronha.io/a-simple-state-machine-framework-in-go
|
||
|
// Many thanks to the author, Venil Norohnha
|
||
|
|
||
|
// ErrEventRejected is the error returned when the state machine cannot process
|
||
|
// an event in the state that it is in.
|
||
|
var ErrEventRejected = errors.New("event rejected")
|
||
|
|
||
|
const (
|
||
|
// Default represents the default state of the system.
|
||
|
Default StateType = ""
|
||
|
|
||
|
// NoOp represents a no-op event.
|
||
|
NoOp EventType = "NoOp"
|
||
|
)
|
||
|
|
||
|
// StateType represents an extensible state type in the state machine.
|
||
|
type StateType string
|
||
|
|
||
|
// EventType represents an extensible event type in the state machine.
|
||
|
type EventType string
|
||
|
|
||
|
// EventContext represents the context to be passed to the action implementation.
|
||
|
type EventContext interface{}
|
||
|
|
||
|
// Action represents the action to be executed in a given state.
|
||
|
type Action interface {
|
||
|
Execute(eventCtx EventContext) EventType
|
||
|
}
|
||
|
|
||
|
// Events represents a mapping of events and states.
|
||
|
type Events map[EventType]StateType
|
||
|
|
||
|
// State binds a state with an action and a set of events it can handle.
|
||
|
type State struct {
|
||
|
Action Action
|
||
|
Events Events
|
||
|
}
|
||
|
|
||
|
// States represents a mapping of states and their implementations.
|
||
|
type States map[StateType]State
|
||
|
|
||
|
// StateMachine represents the state machine.
|
||
|
type StateMachine struct {
|
||
|
// Previous represents the previous state.
|
||
|
Previous StateType
|
||
|
|
||
|
// Current represents the current state.
|
||
|
Current StateType
|
||
|
|
||
|
// States holds the configuration of states and events handled by the state machine.
|
||
|
States States
|
||
|
|
||
|
// mutex ensures that only 1 event is processed by the state machine at any given time.
|
||
|
mutex sync.Mutex
|
||
|
}
|
||
|
|
||
|
// getNextState returns the next state for the event given the machine's current
|
||
|
// state, or an error if the event can't be handled in the given state.
|
||
|
func (s *StateMachine) getNextState(event EventType) (StateType, error) {
|
||
|
if state, ok := s.States[s.Current]; ok {
|
||
|
if state.Events != nil {
|
||
|
if next, ok := state.Events[event]; ok {
|
||
|
return next, nil
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return Default, ErrEventRejected
|
||
|
}
|
||
|
|
||
|
// SendEvent sends an event to the state machine.
|
||
|
func (s *StateMachine) SendEvent(event EventType, eventCtx EventContext) error {
|
||
|
s.mutex.Lock()
|
||
|
defer s.mutex.Unlock()
|
||
|
|
||
|
for {
|
||
|
// Determine the next state for the event given the machine's current state.
|
||
|
nextState, err := s.getNextState(event)
|
||
|
if err != nil {
|
||
|
return ErrEventRejected
|
||
|
}
|
||
|
|
||
|
// Identify the state definition for the next state.
|
||
|
state, ok := s.States[nextState]
|
||
|
if !ok || state.Action == nil {
|
||
|
// configuration error
|
||
|
}
|
||
|
|
||
|
// Transition over to the next state.
|
||
|
s.Previous = s.Current
|
||
|
s.Current = nextState
|
||
|
|
||
|
// Execute the next state's action and loop over again if the event returned
|
||
|
// is not a no-op.
|
||
|
nextEvent := state.Action.Execute(eventCtx)
|
||
|
if nextEvent == NoOp {
|
||
|
return nil
|
||
|
}
|
||
|
event = nextEvent
|
||
|
}
|
||
|
}
|