stateless

package module
v1.0.1 Latest Latest
Warning

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

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

README

stateless

Documentation Build Status Go Report Card codecov codeclimate License Mentioned in Awesome Go

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, "qmuntal")

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#.

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.

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:

stateMachine := 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.

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, "qmuntal")

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.Fire(triggerCallDialed, "qmuntal")
	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 %s\n", phoneCall.MustState())
	phoneCall.ToGraph()

}

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 : [qmuntal]
[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(context.Context, ...interface{}) error

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

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(context.Context, ...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, destinationSelector func(context.Context, ...interface{}) (State, error),
	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. Reading and writing to the state store is protected by mutual exclusion locks, therefore it is safe to use the StateMachine concurrently. All the other actions (OnEntry, OnActivate, ...) are not thread-safe and is up to the client to decide if it is necessary to protected them.

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 currrent 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) (bool, error)

CanFire see CanFireCtx.

func (*StateMachine) CanFireCtx

func (sm *StateMachine) CanFireCtx(ctx context.Context, trigger Trigger) (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 currrent 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.

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(onTransitionAction func(context.Context, Transition))

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

func (*StateMachine) OnUnhandledTrigger

func (sm *StateMachine) OnUnhandledTrigger(onUnhandledTriggerAction 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{}) ([]Trigger, error)

PermittedTriggers see PermittedTriggersCtx.

func (*StateMachine) PossibleTriggersCtx

func (sm *StateMachine) PossibleTriggersCtx(ctx context.Context, args ...interface{}) ([]Trigger, 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
}

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 Trigger

type Trigger = interface{}

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

type TriggerWithParameters

type TriggerWithParameters struct {
	Trigger       Trigger
	ArgumentTypes []reflect.Type
}

TriggerWithParameters associates configured parameters with an underlying trigger value.

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