Documentation
¶
Overview ¶
Package fsm manages the behaviour of a Finite State Machine.
Basics ¶
We create an FSM in two stages - firstly we create a set of allowed state transitions:
st, err := fsm.NewStateTrans("name",
fsm.STPair{fsm.InitState, "start"},
fsm.STPair{"start", "finish"})
Then we create the FSM itself giving it the set of allowed state transitions and an associated Underlying (any type which satisfies the Underlying interface):
f := fsm.New(st, myThing)
This FSM will then enforce the rules given in the Transitions object. Additional rules can be applied through the TransitionAllowed function on the Underlying object.
The intention is that there can be multiple instances of an FSM all sharing the same set of allowed state transitions
Each FSM has an associated underlying (which can be nil). This represents an object whose state is being managed by the FSM. This underlying, if present, must satisfy the Underlying interface.
The default starting state of every FSM is given by the fsm.InitState const
Example (Ex1) ¶
Example_ex1 is an example of how the fsm can be used to manage the lifecycle of some business object; in this case, a bug.
package main
import (
"fmt"
"github.com/nickwells/fsm.mod/fsm"
)
const (
FixInProgress = "FixInProgress"
ReadyToFix = "ReadyToFix"
ReadyToReview = "ReadyToReview"
ReadyToTest = "ReadyToTest"
Rejected = "Rejected"
Released = "Released"
TestFailed = "TestFailed"
TestInProgress = "TestInProgress"
TestPassed = "TestPassed"
UnderReview = "UnderReview"
)
var transitionToChecks = map[string]func(b *BugReport, f *fsm.FSM) error{
TestPassed: checkTestEvidence,
}
// BugReport is a toy structure representing a bug. The key point of
// interest is that we embed the FSM in the structure. This allows us to
// call FSM methods on the BugReport objects directly
type BugReport struct {
*fsm.FSM
name string
description string
testEvidence string
}
// checkTestEvidence returns an error if the bug report doesn't have test
// evidence
func checkTestEvidence(b *BugReport, _ *fsm.FSM) error {
if b.testEvidence == "" {
return fmt.Errorf("missing test evidence")
}
return nil
}
// TransitionAllowed satisfies the Underlying interface. It can be used to
// perform extra checks on state transitions in addition to the checks that
// the FSM will perform. Here we make sure that there is test evidence
// attached to the bug before we allow the state to be changed to TestPassed
func (b *BugReport) TransitionAllowed(f *fsm.FSM, newState string) error {
if cf, ok := transitionToChecks[newState]; ok {
err := cf(b, f)
if err != nil {
return err
}
}
if newState == TestPassed {
if b.testEvidence == "" {
return fmt.Errorf("missing test evidence")
}
}
return nil
}
// OnTransition satisfies the Underlying interface. It can be used to
// perform custom actions when certain states are reached or when states are
// left. Here we notify the release team when the tests for a bugfix have
// all passed
func (b *BugReport) OnTransition(f *fsm.FSM) {
if f.CurrentState() == TestPassed {
notifyReleaseTeam(b)
}
}
// SetFSM satisfies the Underlying interface. It can be used to set the
// underlying's embedded FSM as here
func (b *BugReport) SetFSM(f *fsm.FSM) {
b.FSM = f
}
// notifyReleaseTeam will tell the release team that the bugfix is ready
// to be released
func notifyReleaseTeam(b *BugReport) {
fmt.Println("The fix to bug:", b.name,
"(", b.description, ") is ready to release")
}
// Example_ex1 is an example of how the fsm can be used to manage the
// lifecycle of some business object; in this case, a bug.
func main() {
// now construct the set of allowed state transitions
st, err := fsm.NewStateTrans("BugReport", []fsm.STPair{
{fsm.InitState, ReadyToReview},
{ReadyToReview, UnderReview},
{UnderReview, ReadyToReview},
{UnderReview, Rejected},
{UnderReview, ReadyToFix},
{ReadyToFix, UnderReview},
{ReadyToFix, FixInProgress},
{FixInProgress, ReadyToFix},
{FixInProgress, ReadyToTest},
{ReadyToTest, TestInProgress},
{TestInProgress, TestPassed},
{TestInProgress, TestFailed},
{TestFailed, FixInProgress},
{TestPassed, Released},
}...)
if err != nil {
fmt.Println("There was a problem initialising the transitions:", err)
return
}
_ = st.SetStateDesc(ReadyToReview,
"The bug is ready to review")
_ = st.SetStateDesc(UnderReview,
"The bug is being reviewed")
_ = st.SetStateDesc(Rejected,
"The bug has been rejected")
_ = st.SetStateDesc(ReadyToFix,
"The bug has been accepted and work can start")
_ = st.SetStateDesc(FixInProgress,
"The bug is being worked on")
_ = st.SetStateDesc(ReadyToTest,
"The bug has been fixed and is ready to test")
_ = st.SetStateDesc(TestInProgress,
"The bug is being tested")
_ = st.SetDescriptions(
fsm.StateDesc{TestPassed, "The bug fix has passed the tests"},
fsm.StateDesc{TestFailed, "The bug fix has failed the tests"},
fsm.StateDesc{Released, "The bug fix has been released"})
b := BugReport{name: "ID-1234"}
// now we can create a new FSM which allows state transitions as given
// by 'st' and with an underlying of 'b'. The SetFSM function on 'b'
// will be called
fsm.New(st, &b)
sampleStateChanges := []string{
ReadyToReview,
"Any",
Released,
UnderReview,
Rejected,
}
fmt.Println("Current state:", b.CurrentState())
for _, newState := range sampleStateChanges {
fmt.Println("Changing to: ", newState)
err := b.ChangeState(newState)
if err != nil {
fmt.Println("ERROR: Cannot change from",
b.CurrentState(), "to", newState, ": ", err)
}
fmt.Println("Current state:", b.CurrentState())
if b.IsInTerminalState() {
fmt.Println("done")
break
}
}
}
Output: Current state: init Changing to: ReadyToReview Current state: ReadyToReview Changing to: Any ERROR: Cannot change from ReadyToReview to Any : FSM: "BugReport": "Any" is not a known state Current state: ReadyToReview Changing to: Released ERROR: Cannot change from ReadyToReview to Released : FSM: "BugReport": There is no valid transition from "ReadyToReview" to "Released" Current state: ReadyToReview Changing to: UnderReview Current state: UnderReview Changing to: Rejected Current state: Rejected done
Example (Ex2) ¶
Example_ex2 is an example of how the StateTrans struct can be used
// now construct the set of allowed state transitions
st, err := fsm.NewStateTrans("my \"best\" ST graph", []fsm.STPair{
{fsm.InitState, ReadyToReview},
{ReadyToReview, UnderReview},
{UnderReview, ReadyToReview},
{UnderReview, Rejected},
{UnderReview, ReadyToFix},
{ReadyToFix, UnderReview},
{ReadyToFix, FixInProgress},
{FixInProgress, ReadyToFix},
{FixInProgress, ReadyToTest},
{ReadyToTest, TestInProgress},
{TestInProgress, TestPassed},
{TestInProgress, TestFailed},
{TestFailed, FixInProgress},
{TestPassed, Released},
}...)
if err != nil {
fmt.Println("There was a problem initialising the transitions:", err)
return
}
st.PrintDot(os.Stdout)
Output: // A state transition graph for // my "best" ST graph digraph st { node [shape = doublecircle style=filled fillcolor=lightblue]; "init"; node [shape = doublecircle style=filled fillcolor=grey85]; "Rejected", "Released"; node [shape = circle style=solid]; { rank = same; "Rejected" "Released" } "FixInProgress" -> "ReadyToFix" "FixInProgress" -> "ReadyToTest" "ReadyToFix" -> "FixInProgress" "ReadyToFix" -> "UnderReview" "ReadyToReview" -> "UnderReview" "ReadyToTest" -> "TestInProgress" "TestFailed" -> "FixInProgress" "TestInProgress" -> "TestFailed" "TestInProgress" -> "TestPassed" "TestPassed" -> "Released" "UnderReview" -> "ReadyToFix" "UnderReview" -> "ReadyToReview" "UnderReview" -> "Rejected" "init" -> "ReadyToReview" fontsize=22 label = " my \"best\" ST graph " }
Index ¶
- Constants
- type Error
- type FSM
- func (f *FSM) ChangeState(newState string) error
- func (f *FSM) CurrentState() string
- func (f FSM) Format(fstate fmt.State, c rune)
- func (f *FSM) IsInInitialState() bool
- func (f *FSM) IsInTerminalState() bool
- func (f *FSM) Name() string
- func (f *FSM) NextStates() []string
- func (f *FSM) PriorState() string
- type ForbiddenChange
- type NoTransition
- type STPair
- type StateDesc
- type StateTrans
- type Underlying
- type UnknownState
Examples ¶
Constants ¶
const InitState = "init"
InitState is the initial state of a finite state machine
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Error ¶ added in v1.1.0
type Error interface {
error
// FSMError is a no-op function but it serves to
// distinguish errors from this package from other errors
FSMError()
}
Error is the type of an error from this package
type FSM ¶
type FSM struct {
// contains filtered or unexported fields
}
FSM represents a Finite State Machine
func New ¶
func New(st *StateTrans, u Underlying) *FSM
New creates a new Finite State Machine. It returns nil if the StateTrans is nil.
The prior and current states are set to InitState.
The SetFSM method on the Underlying is called with the new FSM so that the Underlying can store the associated FSM if required.
func (*FSM) ChangeState ¶
ChangeState changes the state from the current state to the new state provided the new state is a valid transition from the current state of the FSM and the transition is allowed by the Underlying TransitionAllowed function. Following the change of state the Underlying OnTransition function is called
func (*FSM) CurrentState ¶
CurrentState returns the name of the current state of the FSM
func (FSM) Format ¶
Format is used by the fmt package in the standard library to format the FSM. It supports two formats:
%s which prints the current state %v which prints the current state with a label "State: "
Either of these can be given the '#' flag which causes them to also print the FSM name and the previous state. Additionally the %s format will print any state descriptions.
func (*FSM) IsInInitialState ¶
IsInInitialState returns true if the FSM is in the initial state
func (*FSM) IsInTerminalState ¶
IsInTerminalState returns true if the FSM is in a terminal state
func (*FSM) NextStates ¶
NextStates returns a sorted slice containing the names of the valid next states of the FSM
func (*FSM) PriorState ¶
PriorState returns the name of the prior state of the FSM
type ForbiddenChange ¶ added in v1.1.0
ForbiddenChange is an error type that represents a forbidden transition between states. This is the case where the fsm would allow the transition but the underlying check prevents it (TransitionAllowed returns a non-nil error)
func (ForbiddenChange) Error ¶ added in v1.1.0
func (fe ForbiddenChange) Error() string
Error returns a string form of the error
func (ForbiddenChange) FSMError ¶ added in v1.1.0
func (ForbiddenChange) FSMError()
FSMError is a no-op function used to distinguish between an fsm package error and other error types.
func (ForbiddenChange) Unwrap ¶ added in v1.1.0
func (fe ForbiddenChange) Unwrap() error
Unwrap returns the underlying error
type NoTransition ¶ added in v1.1.0
NoTransition is an error type that represents a non-existent transition between states. This is the case where the fsm has no single step between the two states.
func (NoTransition) Error ¶ added in v1.1.0
func (fe NoTransition) Error() string
Error returns a string form of the error
func (NoTransition) FSMError ¶ added in v1.1.0
func (NoTransition) FSMError()
FSMError is a no-op function used to distinguish between an fsm package error and other error types.
type STPair ¶
type STPair struct {
From, To string
}
STPair represents a state transition. A slice of these should be passed to the NewStateTrans method in order to set the valid transitions.
type StateTrans ¶
type StateTrans struct {
// contains filtered or unexported fields
}
StateTrans records the valid state changes.
func NewStateTrans ¶
func NewStateTrans(name string, transitions ...STPair) (*StateTrans, error)
NewStateTrans creates a new set of State transitions. The allowed transitions must be set at creation time by passing STPair's. If setting the transitions from the passed slice returns an error then this function will return a nil StateTrans and the error, otherwise the newly created StateTrans is returned and a nil error.
Note that the order of creation of states is important - states must be created before a transition can be created from them. Every set of state transitions has a starting state given by the fsm.InitState const. New states will be created automatically if the 'to' state doesn't exist but the 'from' state must always exist. The state named from the InitState constant is always present in each StateTrans.
The name has no semantic meaning and is only used for documentation purposes.
func (StateTrans) HasState ¶
func (st StateTrans) HasState(name string) bool
HasState return true if the StateTrans object contains a state with the given name.
func (StateTrans) Name ¶
func (st StateTrans) Name() string
Name returns the name of the collection of StateTrans
func (StateTrans) PrintDot ¶
func (st StateTrans) PrintDot(w io.Writer)
PrintDot prints the state transitions as a directed graph in the graphviz DOT language. The output of this func can be interpreted by the dot command (on Linux). To generate a png file from this you could write the output to a file called, for instance, stateTrans.gv and then use the following command to generate the png image and write it to a file called graph.png
dot -Tpng -ograph.png stateTrans.gv
This might be useful for generating documentation for your package.
func (*StateTrans) SetDescriptions ¶
func (st *StateTrans) SetDescriptions(descriptions ...StateDesc) error
SetDescriptions sets the state descriptions from the values given in the slice of state descriptions. It will return an error if any state does not exist and will set the error state on the StateTrans. It will also return an error if the StateTrans already has its error state set.
func (*StateTrans) SetStateDesc ¶
func (st *StateTrans) SetStateDesc(name, desc string) error
SetStateDesc sets the state description. It will return an error if the named state does not exist.
func (StateTrans) StateCount ¶
func (st StateTrans) StateCount() int
StateCount returns a count of the number of states
type Underlying ¶
type Underlying interface {
// TransitionAllowed is called before the change of state to the newState.
// If it returns an error the transition is not performed
TransitionAllowed(f *FSM, newState string) error
// OnTransition is called after the state of the FSM has been successsfully
// changed from the old state to the new state
OnTransition(f *FSM)
// SetFSM is called when the Underlying object is set on the FSM. It
// provides a way for the Underlying object to know the FSM(s) with which
// it is associated.
//
// Note that an Underlying could be associated with multiple FSMs at the
// same time in which case a map from FSM name to FSM pointer might be
// appropriate
//
// if the Underlying has only a single associated FSM then the FSM pointer
// could be embedded in the Underlying. This would allow the FSM methods to
// be called on the Underlying directly
SetFSM(f *FSM)
}
Underlying is an interface representing a set of functions to be called around various FSM transitions. The Underlying can be used to represent an object whose state is managed by the FSM
type UnknownState ¶ added in v1.1.0
UnknownState is an error type that represents an unknown state
func (UnknownState) Error ¶ added in v1.1.0
func (fe UnknownState) Error() string
Error returns a string form of the error
func (UnknownState) FSMError ¶ added in v1.1.0
func (UnknownState) FSMError()
FSMError is a no-op function used to distinguish between an fsm package error and other error types.