stateless

package module
v1.5.32 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2021 License: BSD-2-Clause Imports: 8 Imported by: 0

README

go.dev Build Status Code Coverage Go Report Card Licenses Mentioned in Awesome Go

Stateless

Create state machines and lightweight state machine-based workflows directly in Go code:

phoneCall := stateless.NewStateMachine(stateOffHook)

phoneCall.Configure(stateOffHook).Permit(triggerCallDialed, stateRinging)

phoneCall.Configure(stateRinging).
  OnEntryFrom(triggerCallDialed, func(_ context.Context, args ...interface{}) error {
    onDialed(args[0].(string))
    return nil
  }).
  Permit(triggerCallConnected, stateConnected)

phoneCall.Configure(stateConnected).
  OnEntry(func(_ context.Context, _ ...interface{}) error {
    startCallTimer()
    return nil
  }).
  OnExit(func(_ context.Context, _ ...interface{}) error {
    stopCallTimer()
    return nil
  }).
  Permit(triggerLeftMessage, stateOffHook).
  Permit(triggerPlacedOnHold, stateOnHold)

// ...

phoneCall.Fire(triggerCallDialed, "gustavoaborges")

This project, as well as the example above, is almost a direct, yet idiomatic, port of dotnet-state-machine/stateless, which is written in C#.

The state machine implemented in this library is based on the theory of UML statechart. The concepts behind it are about organizing the way a device, computer program, or other (often technical) process works such that an entity or each of its sub-entities is always in exactly one of a number of possible states and where there are well-defined conditional transitions between these states.

Features

Most standard state machine constructs are supported:

  • Support for states and triggers of any comparable type (int, strings, boolean, structs, etc.)
  • Hierarchical states
  • Entry/exit events for states
  • Guard clauses to support conditional transitions
  • Introspection

Some useful extensions are also provided:

  • Ability to store state externally (for example, in a property tracked by an ORM)
  • Parameterised triggers
  • Reentrant states
  • Thread-safe
  • Export to DOT graph
Hierarchical States

In the example below, the OnHold state is a substate of the Connected state. This means that an OnHold call is still connected.

phoneCall.Configure(stateOnHold).
  SubstateOf(stateConnected).
  Permit(triggerTakenOffHold, stateConnected).
  Permit(triggerPhoneHurledAgainstWall, statePhoneDestroyed)

In addition to the StateMachine.State property, which will report the precise current state, an IsInState(State) method is provided. IsInState(State) will take substates into account, so that if the example above was in the OnHold state, IsInState(State.Connected) would also evaluate to true.

Entry/Exit Events

In the example, the StartCallTimer() method will be executed when a call is connected. The StopCallTimer() will be executed when call completes (by either hanging up or hurling the phone against the wall.)

The call can move between the Connected and OnHold states without the StartCallTimer() and StopCallTimer() methods being called repeatedly because the OnHold state is a substate of the Connected state.

Entry/Exit event handlers can be supplied with a parameter of type Transition that describes the trigger, source and destination states.

Initial state transitions

A substate can be marked as initial state. When the state machine enters the super state it will also automatically enter the substate. This can be configured like this:

sm.Configure(State.B)
  .InitialTransition(State.C);

sm.Configure(State.C)
  .SubstateOf(State.B);
External State Storage

Stateless is designed to be embedded in various application models. For example, some ORMs place requirements upon where mapped data may be stored, and UI frameworks often require state to be stored in special "bindable" properties. To this end, the StateMachine constructor can accept function arguments that will be used to read and write the state values:

machine := stateless.NewStateMachineWithExternalStorage(func(_ context.Context) (stateless.State, error) {
  return myState.Value, nil
}, func(_ context.Context, state stateless.State) error {
  myState.Value  = state
  return nil
}, stateless.FiringQueued)

In this example the state machine will use the myState object for state storage.

Activation / Deactivation

It might be necessary to perform some code before storing the object state, and likewise when restoring the object state. Use Deactivate and Activate for this. Activation should only be called once before normal operation starts, and once before state storage.

Introspection

The state machine can provide a list of the triggers that can be successfully fired within the current state via the StateMachine.PermittedTriggers property.

Guard Clauses

The state machine will choose between multiple transitions based on guard clauses, e.g.:

phoneCall.Configure(stateOffHook).
  Permit(triggerCallDialled, stateRinging, func(_ context.Context, _ ...interface{}) bool {
    return IsValidNumber()
  }).
  Permit(triggerCallDialled, stateBeeping, func(_ context.Context, _ ...interface{}) bool {
    return !IsValidNumber()
  })

Guard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time). Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate.

The guard clauses will be evaluated whenever a trigger is fired. Guards should therefor be made side effect free.

Parameterised Triggers

Strongly-typed parameters can be assigned to triggers:

stateMachine.SetTriggerParameters(triggerCallDialed, reflect.TypeOf(""))

stateMachine.Configure(stateRinging).
  OnEntryFrom(triggerCallDialed, func(_ context.Context, args ...interface{}) error {
    fmt.Println(args[0].(string))
    return nil
  })

stateMachine.Fire(triggerCallDialed, "gustavoaborges")

It is runtime safe to cast parameters to the ones specified in SetTriggerParameters. If the parameters passed in Fire do not match the ones specified it will panic.

Trigger parameters can be used to dynamically select the destination state using the PermitDynamic() configuration method.

Ignored Transitions and Reentrant States

Firing a trigger that does not have an allowed transition associated with it will cause a panic to be thrown.

To ignore triggers within certain states, use the Ignore(Trigger) directive:

phoneCall.Configure(stateConnected).
  Ignore(triggerCallDialled)

Alternatively, a state can be marked reentrant so its entry and exit events will fire even when transitioning from/to itself:

stateMachine.Configure(stateAssigned).
  PermitReentry(triggerAssigned).
  OnEntry(func(_ context.Context, _ ...interface{}) error {
    startCallTimer()
    return nil
  })

By default, triggers must be ignored explicitly. To override Stateless's default behaviour of throwing a panic when an unhandled trigger is fired, configure the state machine using the OnUnhandledTrigger method:

stateMachine.OnUnhandledTrigger( func (_ context.Context, state State, _ Trigger, _ []string) {})
Export to DOT graph

It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.

sm := stateMachine.Configure(stateOffHook).
  Permit(triggerCallDialed, stateRinging, isValidNumber)
graph := sm.ToGraph()

The StateMachine.ToGraph() method returns a string representation of the state machine in the DOT graph language, e.g.:

digraph {
  OffHook -> Ringing [label="CallDialled [isValidNumber]"];
}

This can then be rendered by tools that support the DOT graph language, such as the dot command line tool from graphviz.org or viz.js. See webgraphviz.com for instant gratification. Command line example: dot -T pdf -o phoneCall.pdf phoneCall.dot to generate a PDF file.

This is the complete Phone Call graph as builded in example_test.go.

Phone Call graph

Project Goals

This page is an almost-complete description of Stateless, and its explicit aim is to remain minimal.

Please use the issue tracker or the if you'd like to report problems or discuss features.

(Why the name? Stateless implements the set of rules regarding state transitions, but, at least when the delegate version of the constructor is used, doesn't maintain any internal state itself.)

Documentation

Overview

Example
package main

import (
	"context"
	"fmt"
	"reflect"

	"github.com/gustavoaborges/stateless"
)

const (
	triggerCallDialed             = "CallDialed"
	triggerCallConnected          = "CallConnected"
	triggerLeftMessage            = "LeftMessage"
	triggerPlacedOnHold           = "PlacedOnHold"
	triggerTakenOffHold           = "TakenOffHold"
	triggerPhoneHurledAgainstWall = "PhoneHurledAgainstWall"
	triggerMuteMicrophone         = "MuteMicrophone"
	triggerUnmuteMicrophone       = "UnmuteMicrophone"
	triggerSetVolume              = "SetVolume"
)

const (
	stateOffHook        = "OffHook"
	stateRinging        = "Ringing"
	stateConnected      = "Connected"
	stateOnHold         = "OnHold"
	statePhoneDestroyed = "PhoneDestroyed"
)

func main() {
	phoneCall := stateless.NewStateMachine(stateOffHook)
	phoneCall.SetTriggerParameters(triggerSetVolume, reflect.TypeOf(0))
	phoneCall.SetTriggerParameters(triggerCallDialed, reflect.TypeOf(""))

	phoneCall.Configure(stateOffHook).
		Permit(triggerCallDialed, stateRinging)

	phoneCall.Configure(stateRinging).
		OnEntryFrom(triggerCallDialed, func(_ context.Context, args ...interface{}) error {
			onDialed(args[0].(string))
			return nil
		}).
		Permit(triggerCallConnected, stateConnected)

	phoneCall.Configure(stateConnected).
		OnEntry(startCallTimer).
		OnExit(func(_ context.Context, _ ...interface{}) error {
			stopCallTimer()
			return nil
		}).
		InternalTransition(triggerMuteMicrophone, func(_ context.Context, _ ...interface{}) error {
			onMute()
			return nil
		}).
		InternalTransition(triggerUnmuteMicrophone, func(_ context.Context, _ ...interface{}) error {
			onUnmute()
			return nil
		}).
		InternalTransition(triggerSetVolume, func(_ context.Context, args ...interface{}) error {
			onSetVolume(args[0].(int))
			return nil
		}).
		Permit(triggerLeftMessage, stateOffHook).
		Permit(triggerPlacedOnHold, stateOnHold)

	phoneCall.Configure(stateOnHold).
		SubstateOf(stateConnected).
		Permit(triggerTakenOffHold, stateConnected).
		Permit(triggerPhoneHurledAgainstWall, statePhoneDestroyed)

	phoneCall.ToGraph()

	phoneCall.Fire(triggerCallDialed, "gustavoaborges")
	phoneCall.Fire(triggerCallConnected)
	phoneCall.Fire(triggerSetVolume, 2)
	phoneCall.Fire(triggerPlacedOnHold)
	phoneCall.Fire(triggerMuteMicrophone)
	phoneCall.Fire(triggerUnmuteMicrophone)
	phoneCall.Fire(triggerTakenOffHold)
	phoneCall.Fire(triggerSetVolume, 11)
	phoneCall.Fire(triggerPlacedOnHold)
	phoneCall.Fire(triggerPhoneHurledAgainstWall)
	fmt.Printf("State is %v\n", phoneCall.MustState())

}

func onSetVolume(volume int) {
	fmt.Printf("Volume set to %d!\n", volume)
}

func onUnmute() {
	fmt.Println("Microphone unmuted!")
}

func onMute() {
	fmt.Println("Microphone muted!")
}

func onDialed(callee string) {
	fmt.Printf("[Phone Call] placed for : [%s]\n", callee)
}

func startCallTimer(_ context.Context, _ ...interface{}) error {
	fmt.Println("[Timer:] Call started at 11:00am")
	return nil
}

func stopCallTimer() {
	fmt.Println("[Timer:] Call ended at 11:30am")
}
Output:

[Phone Call] placed for : [gustavoaborges]
[Timer:] Call started at 11:00am
Volume set to 2!
Microphone muted!
Microphone unmuted!
Volume set to 11!
[Timer:] Call ended at 11:30am
State is PhoneDestroyed

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultUnhandledTriggerAction

func DefaultUnhandledTriggerAction(_ context.Context, state State, trigger Trigger, unmetGuards []string) error

DefaultUnhandledTriggerAction is the default unhandled trigger action.

Types

type ActionFunc

type ActionFunc = func(ctx context.Context, args ...interface{}) error

ActionFunc describes a generic action function. The context will always contain Transition information.

type DestinationSelectorFunc added in v1.5.4

type DestinationSelectorFunc = func(ctx context.Context, args ...interface{}) (State, error)

DestinationSelectorFunc defines a functions that is called to select a dynamic destination.

type FiringMode

type FiringMode uint8

FiringMode enumerate the different modes used when Fire-ing a trigger.

const (
	// FiringQueued mode shoud be used when run-to-completion is required. This is the recommended mode.
	FiringQueued FiringMode = iota
	// FiringImmediate should be used when the queing of trigger events are not needed.
	// Care must be taken when using this mode, as there is no run-to-completion guaranteed.
	FiringImmediate
)

type GuardFunc

type GuardFunc = func(ctx context.Context, args ...interface{}) bool

GuardFunc defines a generic guard function.

type PossibleTrigger

type PossibleTrigger struct {
	Trigger     Trigger
	UnmetGuards []string
}

type State

type State = interface{}

State is used to to represent the possible machine states.

type StateConfiguration

type StateConfiguration struct {
	// contains filtered or unexported fields
}

StateConfiguration is the configuration for a single state value.

func (*StateConfiguration) Ignore

func (sc *StateConfiguration) Ignore(trigger Trigger, guards ...GuardFunc) *StateConfiguration

Ignore the specified trigger when in the configured state, if the guards return true.

func (*StateConfiguration) InitialTransition

func (sc *StateConfiguration) InitialTransition(targetState State) *StateConfiguration

InitialTransition adds internal transition to this state. When entering the current state the state machine will look for an initial transition, and enter the target state.

func (*StateConfiguration) InternalTransition

func (sc *StateConfiguration) InternalTransition(trigger Trigger, action ActionFunc, guards ...GuardFunc) *StateConfiguration

InternalTransition add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine.

func (*StateConfiguration) Machine

func (sc *StateConfiguration) Machine() *StateMachine

Machine that is configured with this configuration.

func (*StateConfiguration) OnActive

func (sc *StateConfiguration) OnActive(action func(context.Context) error) *StateConfiguration

OnActive specify an action that will execute when activating the configured state.

func (*StateConfiguration) OnDeactivate

func (sc *StateConfiguration) OnDeactivate(action func(context.Context) error) *StateConfiguration

OnDeactivate specify an action that will execute when deactivating the configured state.

func (*StateConfiguration) OnEntry

func (sc *StateConfiguration) OnEntry(action ActionFunc) *StateConfiguration

OnEntry specify an action that will execute when transitioning into the configured state.

func (*StateConfiguration) OnEntryFrom

func (sc *StateConfiguration) OnEntryFrom(trigger Trigger, action ActionFunc) *StateConfiguration

OnEntryFrom Specify an action that will execute when transitioning into the configured state from a specific trigger.

func (*StateConfiguration) OnExit

func (sc *StateConfiguration) OnExit(action ActionFunc) *StateConfiguration

OnExit specify an action that will execute when transitioning from the configured state.

func (*StateConfiguration) Permit

func (sc *StateConfiguration) Permit(trigger Trigger, destinationState State, guards ...GuardFunc) *StateConfiguration

Permit accept the specified trigger and transition to the destination state if the guard conditions are met (if any).

func (*StateConfiguration) PermitDynamic

func (sc *StateConfiguration) PermitDynamic(trigger Trigger, selector DestinationSelectorFunc, guards ...GuardFunc) *StateConfiguration

PermitDynamic accept the specified trigger and transition to the destination state, calculated dynamically by the supplied function.

func (*StateConfiguration) PermitReentry

func (sc *StateConfiguration) PermitReentry(trigger Trigger, guards ...GuardFunc) *StateConfiguration

PermitReentry accept the specified trigger, execute exit actions and re-execute entry actions. Reentry behaves as though the configured state transitions to an identical sibling state. Applies to the current state only. Will not re-execute superstate actions, or cause actions to execute transitioning between super- and sub-states.

func (*StateConfiguration) State

func (sc *StateConfiguration) State() State

State is configured with this configuration.

func (*StateConfiguration) SubstateOf

func (sc *StateConfiguration) SubstateOf(superstate State) *StateConfiguration

SubstateOf sets the superstate that the configured state is a substate of. Substates inherit the allowed transitions of their superstate. When entering directly into a substate from outside of the superstate, entry actions for the superstate are executed. Likewise when leaving from the substate to outside the supserstate, exit actions for the superstate will execute.

type StateMachine

type StateMachine struct {
	// contains filtered or unexported fields
}

A StateMachine is an abstract machine that can be in exactly one of a finite number of states at any given time. It is safe to use the StateMachine concurrently, but non of the callbacks (state manipulation, actions, events, ...) are guarded, so it is up to the client to protect them against race conditions.

func NewStateMachine

func NewStateMachine(initialState State) *StateMachine

NewStateMachine returns a queued state machine.

func NewStateMachineWithExternalStorage

func NewStateMachineWithExternalStorage(stateAccessor func(context.Context) (State, error), stateMutator func(context.Context, State) error, firingMode FiringMode) *StateMachine

NewStateMachineWithExternalStorage returns a state machine with external state storage.

func NewStateMachineWithMode

func NewStateMachineWithMode(initialState State, firingMode FiringMode) *StateMachine

NewStateMachineWithMode returns a state machine with the desired firing mode

func (*StateMachine) Activate

func (sm *StateMachine) Activate() error

Activate see ActivateCtx.

func (*StateMachine) ActivateCtx

func (sm *StateMachine) ActivateCtx(ctx context.Context) error

ActivateCtx activates current state. Actions associated with activating the current state will be invoked. The activation is idempotent and subsequent activation of the same current state will not lead to re-execution of activation callbacks.

func (*StateMachine) CanFire

func (sm *StateMachine) CanFire(trigger Trigger, args ...interface{}) (bool, error)

CanFire see CanFireCtx.

func (*StateMachine) CanFireCtx

func (sm *StateMachine) CanFireCtx(ctx context.Context, trigger Trigger, args ...interface{}) (bool, error)

CanFireCtx returns true if the trigger can be fired in the current state.

func (*StateMachine) Configure

func (sm *StateMachine) Configure(state State) *StateConfiguration

Configure begin configuration of the entry/exit actions and allowed transitions when the state machine is in a particular state.

func (*StateMachine) Deactivate

func (sm *StateMachine) Deactivate() error

Deactivate see DeactivateCtx.

func (*StateMachine) DeactivateCtx

func (sm *StateMachine) DeactivateCtx(ctx context.Context) error

DeactivateCtx deactivates current state. Actions associated with deactivating the current state will be invoked. The deactivation is idempotent and subsequent deactivation of the same current state will not lead to re-execution of deactivation callbacks.

func (*StateMachine) Fire

func (sm *StateMachine) Fire(trigger Trigger, args ...interface{}) error

Fire see FireCtx

func (*StateMachine) FireCtx

func (sm *StateMachine) FireCtx(ctx context.Context, trigger Trigger, args ...interface{}) error

FireCtx transition from the current state via the specified trigger. The target state is determined by the configuration of the current state. Actions associated with leaving the current state and entering the new one will be invoked.

An error is returned if any of the state machine actions or the state callbacks return an error without wrapping. It can also return an error if the trigger is not mapped to any state change, being this error the one returned by `OnUnhandledTrigger` func.

There is no rollback mechanism in case there is an action error after the state has been changed. Guard clauses or error states can be used gracefully handle this situations.

The context is passed down to all actions and callbacks called within the scope of this method. There is no context error checking, although it may be implemented in future releases.

func (*StateMachine) Firing added in v1.5.4

func (sm *StateMachine) Firing() bool

Firing returns true when the state machine is processing a trigger.

func (*StateMachine) IsInState

func (sm *StateMachine) IsInState(state State) (bool, error)

IsInState see IsInStateCtx.

func (*StateMachine) IsInStateCtx

func (sm *StateMachine) IsInStateCtx(ctx context.Context, state State) (bool, error)

IsInStateCtx determine if the state machine is in the supplied state. Returns true if the current state is equal to, or a substate of, the supplied state.

func (*StateMachine) MustState

func (sm *StateMachine) MustState() State

MustState returns the current state without the error. It is safe to use this method when used together with NewStateMachine or when using NewStateMachineWithExternalStorage with an state accessor that does not return an error.

func (*StateMachine) OnTransitioned

func (sm *StateMachine) OnTransitioned(fn ...TransitionFunc)

OnTransitioned registers a callback that will be invoked every time the state machine successfully finishes a transitions from one state into another.

func (*StateMachine) OnTransitioning added in v1.5.4

func (sm *StateMachine) OnTransitioning(fn ...TransitionFunc)

OnTransitioning registers a callback that will be invoked every time the state machine starts a transitions from one state into another.

func (*StateMachine) OnUnhandledTrigger

func (sm *StateMachine) OnUnhandledTrigger(fn UnhandledTriggerActionFunc)

OnUnhandledTrigger override the default behaviour of returning an error when an unhandled trigger.

func (*StateMachine) PermittedTriggers

func (sm *StateMachine) PermittedTriggers(args ...interface{}) ([]Trigger, error)

PermittedTriggers see PermittedTriggersCtx.

func (*StateMachine) PermittedTriggersCtx

func (sm *StateMachine) PermittedTriggersCtx(ctx context.Context, args ...interface{}) ([]Trigger, error)

PermittedTriggersCtx returns the currently-permissible trigger values.

func (*StateMachine) PossibleTriggers

func (sm *StateMachine) PossibleTriggers(args ...interface{}) ([]PossibleTrigger, error)

PermittedTriggers see PermittedTriggersCtx.

func (*StateMachine) PossibleTriggersCtx

func (sm *StateMachine) PossibleTriggersCtx(ctx context.Context, args ...interface{}) ([]PossibleTrigger, error)

PermittedTriggersCtx returns the currently-permissible trigger values.

func (*StateMachine) SetTriggerParameters

func (sm *StateMachine) SetTriggerParameters(trigger Trigger, argumentTypes ...reflect.Type)

SetTriggerParameters specify the arguments that must be supplied when a specific trigger is fired.

func (*StateMachine) State

func (sm *StateMachine) State(ctx context.Context) (State, error)

State returns the current state.

func (*StateMachine) String

func (sm *StateMachine) String() string

String returns a human-readable representation of the state machine. It is not guaranteed that the order of the PermittedTriggers is the same in consecutive executions.

func (*StateMachine) ToGraph

func (sm *StateMachine) ToGraph() string

ToGraph returns the DOT representation of the state machine. It is not guaranteed that the returned string will be the same in different executions.

type Transition

type Transition struct {
	Source      State
	Destination State
	Trigger     Trigger
	// contains filtered or unexported fields
}

Transition describes a state transition.

func GetTransition

func GetTransition(ctx context.Context) Transition

GetTransition returns the transition from the context. If there is no transition the returned value is empty.

func (*Transition) IsReentry

func (t *Transition) IsReentry() bool

IsReentry returns true if the transition is a re-entry, i.e. the identity transition.

type TransitionFunc added in v1.5.4

type TransitionFunc = func(context.Context, Transition)

type Trigger

type Trigger = interface{}

Trigger is used to represent the triggers that cause state transitions.

type UnhandledTriggerActionFunc

type UnhandledTriggerActionFunc = func(ctx context.Context, state State, trigger Trigger, unmetGuards []string) error

UnhandledTriggerActionFunc defines a function that will be called when a trigger is not handled.

Jump to

Keyboard shortcuts

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