fsm

package
v1.1.29 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 13, 2025 License: MIT Imports: 6 Imported by: 0

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

Examples

Constants

View Source
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

func (f *FSM) ChangeState(newState string) error

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

func (f *FSM) CurrentState() string

CurrentState returns the name of the current state of the FSM

func (FSM) Format

func (f FSM) Format(fstate fmt.State, c rune)

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

func (f *FSM) IsInInitialState() bool

IsInInitialState returns true if the FSM is in the initial state

func (*FSM) IsInTerminalState

func (f *FSM) IsInTerminalState() bool

IsInTerminalState returns true if the FSM is in a terminal state

func (*FSM) Name

func (f *FSM) Name() string

Name returns the name of the Finite State Machine

func (*FSM) NextStates

func (f *FSM) NextStates() []string

NextStates returns a sorted slice containing the names of the valid next states of the FSM

func (*FSM) PriorState

func (f *FSM) PriorState() string

PriorState returns the name of the prior state of the FSM

type ForbiddenChange added in v1.1.0

type ForbiddenChange struct {
	FSMName   string
	FromState string
	ToState   string
	UndError  error
}

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

type NoTransition struct {
	FSMName   string
	FromState string
	ToState   string
}

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 StateDesc

type StateDesc struct {
	Name string
	Desc string
}

StateDesc records a state name and an associated description

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

type UnknownState struct {
	FSMName string
	State   string
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL