esi-state-machine

esi-state-machine

Description

Overview

This class provides a simple base class for the creation of statemachines. The state machine includes an "PREPARE" callback, which is intended to collect the required data or do other tidying up prior to the state transition decision. Furthermore the state machine holds a list of available states ( all of type esi-state ) and a list of transitions between these state.

States

Each State must at least have a name and can have an "ON_ENTER", "ON_LEAVE" or "IN_STATE" method. additional data of the state can be stored in the "data" proeprty. State names must be unique. The Callbacks are called with the STATE object (self) as well as with calling statemachine.

Transitions

Each Transition must have a "from" and a "to" entry, which shall be state names and an "CONDITION" callback which must return true, if all conditions for a state transitions are meat. Additionally the Transition can have an "ON_TRANSITION" callback, which is called when the transition is executed. The prototypes of all these callbacks are documented below.

RUN calls

In order to execute a step of the state machine, the "RUN" method must be called. this also allows to pass parameters on to the "PREPARE" method. Additionally, the state machine can have a "CLEANUP" method, which is called at the end of each "RUN".

Inheritance of Instantiation

State machines can be implemented either by creating a child class of the State Machine and to use a instance of that, or the required Methods can just be overwritten in the object, both is possible.

Persistence

The State Machine does not have its own persistence methods, but it supports an external persistence by: exporting the .data section, the .results section and the name of the current state from the statemachine itself, as well as exporting the .data section and the .results section of all states as JSON. And allowing to import the same data during the re-initialization of the statemachine object.

JSON configuration

Originally the whole state machine was defined to be object oriented and to be configured by object and functions, as well as data and configuration. But as this state machine shall also be usable as a configurable command executor, it shall be possible to create a statemachine from a JSON configuration.

Configuring States

So instead of initializing a STATE object with "ON_ENTER", "IN_STATE" and "ON_LEAVE" functions, it is also possible to provide a table from which a FUNCTOR is generated behind the scenes. the table must have the following form:

{
    lib= <name ob library to require>,
    func= <name of function in that library>,
    arg= {arg1, arg2, arg3, ...} -- list of arguments to pass to the function
}

In order to allow more flexible arguments, there is a "lookup" feature included, which is explained later.

Configuring Transitions

In the same fashion, the "CONDITION" and "ON_TRANSITION" function of a "TRANSITION" can be defined. BUT for the "CONDITION" there is also another possibility, instead of configuring a Functor as described above, the Lua expression library can be used, if a table of the following form is used:

{
    ["$luaxp"]="< expression according to luaxp expression>",
    arg = {arg1, arg2, arg3, ...} -- list of arguments to pass to the function
}

The expression is evaluated in context described below.

Lookup, Expressions and Context

The lookup feature works very straight forwardly. It scans each argument for tables of the form:

{ ["$lookup"]={arg1, arg2, arg3,...}}

and replaces this table with the result of the lookup. The arguments are interpreted consecutively as a path within the context, so that {"config", "main", 1 } translates to context["config"]["main"][1]. If the given number is negative, the it is counted from the end, so that -1 translates to main[#main] and -2 to main[#main-1]. The context for the lookup, as well as for the luaxp function is the same:

{
    data= statemachine.data,
    config=statemachine.config,
    currentState = statemachine.currentState,
    arg= arg,               -- only present and relevant for luaxp
    results= statemachine.results,
    lastResult= statemachine.results[#statemachine.results]
}

Example 1 (json config)

-- state machine definition via a JSON string
local JSON= require("rapidjson")
local ESM = require("esi-state-machine")
local jsonString = JSON.encode( {
        options= {resetResultsOnPrepare= false},
        config= {setting1= true},
        data = {value1 = false},
        startState= "stateOne",
        states= {
                    {
                        name= "stateOne",
                    },
                    {
                        name= "stateTwo",
                        IN_STATE=  {
                            lib= "syslib",
                            func= "getcorepath",
                            arg= {}  -- this must be a list!
                        },
                        ON_LEAVE= {
                            lib = "table",
                            func = "concat",
                            arg= { { {["$lookup"]= {"results", -1, "v"}} , ".ObjectName"}, ""}
                        }
                    },
                    {
                        name= "lastState",
                        ON_ENTER= {
                            lib= "syslib",
                            func= "getvalue",
                            arg= { {["$lookup"]= {"results", -2, "v"}}  }
                        }
                    },
                },
        transitions= {
            {
                from= "stateOne", to= "stateTwo",
                CONDITION= {
                            ["$luaxp"]="(currentState.name == \"stateOne\") && (arg.argument1==false)",
                            arg = {argument1=false}
                        },
            },
            {
                from= "stateTwo", to= "lastState",
                CONDITION= {
                            ["$luaxp"]="(currentState.name == \"stateTwo\") && (arg.corepath != arg.corepath2)",
                            arg= {corepath=syslib.getcorepath(),
                                    corepath2= syslib.getcorepath().." "}
                                }
            }
        }
    })
local esm = ESM( JSON.decode(jsonString))
for i=1,10 do
    esm:RUN()
end
return esm.results

Example 2 (definition via lua objects)

-- state machine definition via lua object
-- this example has the same functionality as the example1 above
local JSON= require("rapidjson")
local ESM = require("esi-state-machine")
local config = {
        options= {resetResultsOnPrepare= false},
        config= {setting1= true},
        data = {value1 = false},
        startState= "stateOne",
        states= {
                    {
                        name= "stateOne",
                    },
                    {
                        name= "stateTwo",
                        IN_STATE=  function(state, sm) -- state := self
                            return syslib.getcorepath()
                        end,
                        ON_LEAVE= function(state, sm) -- state:=self
                            return table.concat( {sm.results[#sm.results].v, ".ObjectName"}, "")
                        end
                    },
                    {
                        name= "lastState",
                        ON_ENTER= function(state, sm)
                            return syslib.getvalue(sm.results[#sm.results-1].v)
                        end
                    },
                },
        transitions= {
            {
                from= "stateOne", to= "stateTwo",
                CONDITION= function(self, sm)
                        return sm.currentState.name == "stateOne"
                end
            },
            {
                from= "stateTwo", to= "lastState",
                CONDITION= function(self, sm)
                    return sm.currentState.name == "stateTwo"
                end
            }
        }
    }
local esm = ESM( config )
for i=1,10 do
    esm:RUN()
end
return esm.results

Example 3 (conditions with luaxp and luaxp2)

-- state machine definition via a JSON string
local JSON= require("rapidjson")
local ESM = require("esi-state-machine")
local jsonString = JSON.encode( {
        options= {resetResultsOnPrepare= false},
        config= {setting1= true},
        data = {value1 = false},
        startState= "stateOne",
        states= {
                    {
                        name= "stateOne",
                    },
                    {
                        name= "stateTwo",
                        IN_STATE=  {
                            lib= "syslib",
                            func= "getcorepath",
                            arg= {}  -- this must be a list!
                        },
                        ON_LEAVE= {
                            lib = "table",
                            func = "concat",
                            arg= { { {["$lookup"]= {"results", -1, "v"}} , ".ObjectName"}, ""}
                        }
                    },
                    {
                        name= "lastState",
                        ON_ENTER= {
                            lib= "syslib",
                            func= "getvalue",
                            arg= { {["$lookup"]= {"results", -2, "v"}}  }
                        }
                    },
                },
        transitions= {
            {
                from= "stateOne", to= "*",
                CONDITION= {
                            ["$luaxp"]="(currentState.name == \"stateOne\") && (arg.argument1==false)",
							["$luaxp2"]="\"state\" + \"Two\"",
                            arg = {argument1=false}
                        },
            },
            {
                from= "stateTwo", to= "*",
                CONDITION= {
                            ["$luaxp"]="(currentState.name == \"stateTwo\") && (arg.corepath != arg.corepath2)",
							["$luaxp2"]="\"last\" + \"State\"",
                            arg= {corepath=syslib.getcorepath(),
                                    corepath2= syslib.getcorepath().." "}
                                }
            }
        }
    })
local esm = ESM( JSON.decode(jsonString))
for i=1,10 do
    esm:RUN()
end
return esm.results

Dependencies

library version inmation core library

rapidjson

5.3.0

yes

esi-class

2.0.0

yes

esi-subtype

1.0.0

yes

luaxp

0.9.7

yes

Documentation

STATE:INIT

Description

STATE(template)

this is the constructor for the STATE object

Parameters

Name Description

template

this is the template for object creation.
This template must follow the form:
{ name= <name of the state>,
  description= <description of the state>, --string, optional
  data= <table to hold state data>, --optional
  ON_ENTER= <function executed when entering the state>, --optional
  IN_STATE= <function called every time the stateMachine runs in this state>, --optional
  ON_LEAVE= <function called, when this state is left>, --optional
}

Returns

returns the STATE object

Examples

local ESM = require("esi-state-machine")
-- this transition automatically changes from state1 to state2
local trans = ESM.STATE({
                  name="state1",
                  data = {inStateCnt = 0},
                  ON_ENTER = function(self, stateMachine) -- called on enter
                      return "state "..self.name.. " entered."
                  end,
                  IN_STATE = function(self, stateMachine) -- called for each run, while in the state
                     self.data.inStateCnt = self.data.inStateCnt +1
                     return "inState "..self.name..": called "..self.data.inStateCnt.." times."
                  end,
                  ON_LEAVE = function(self, stateMachine) -- called on leave
                      return "state "..self.name.." left."
                  end
  })

STATE:TO_MERMAID

Description

STATE:TO_MERMAID

this function generates a mermaid subgraph from the transition

Parameters

Name Description

self

the object to process

id

the id of the state

Returns

returns mermaid subgraph as string.

STATE:ON_ENTER

Description

STATE:ON_ENTER callback of the STATE helper class

This callback is called, whenever a state is entered.
this is executed after the transition was executed

Parameters

Name Description

self

the STATE object itself

stateMachine

the calling statemachine

Returns

nil, no return value needed.

STATE:ON_LEAVE

Description

STATE:ON_LEAVE callback of the STATE helper class

This callback is called, whenever a state is left.
the result of this function is written to the results array prior to the entry/entries from the transition

Parameters

Name Description

self

the STATE object itself

stateMachine

the calling statemachine

Returns

nil, no return value needed.

STATE:IN_STATE

Description

STATE:IN_STATE callback of the STATE helper class

This callback is called, whenever the state machine is in a specific state.
the result of this function is written to the results array.

Parameters

Name Description

self

the STATE object itself

stateMachine

the calling statemachine

Returns

nil, no return value needed.

TRANSITION:INIT

Description

TRANSITION(template)

this is the constructor for the TRANSITION object

Parameters

Name Description

template

this is the template for object creation.
the template must have the following form:
{ from= <name of the old state>,
  to = <name of the new state>,
  CONDITION = < function which must return true, if transition shall be taken >,
  ON_TRANSITION = < function executed during the transition >, --optional
}

Returns

returns the TRANSITION object

Examples

local ESM = require("esi-state-machine")
-- this transition automatically changes from state1 to state2
local trans = ESM.TRANSITION({
                  from = "state1",
                  to =   "state2",
                  CONDITION = function(self, stateMachine)
                         stateMachine.currentState.name == "state1"
                  end,
                  ON_TRANSITION = function(self, stateMachine)
                     retrun "Transition from state1 to state2"
                  end
  })

TRANSITION:TO_MERMAID

Description

TRANSITION:TO_MERMAID

this function generates a mermaid subgraph from the transition

Returns

returns mermaid subgraph as string.

TRANSITION:CONDITION

Description

TRANSITION:CONDITION callback

this function is evaluated in order to decide if a state transition is possible.

Parameters

Name Description

self

the transition object itself.

stateMachine

the esi-state-machine calling this condition evaluation.

Returns

true if the condition for a state transition is meat, false otherwise.

TRANSITION:ON_TRANSITION

Description

TRANSITION:ON_TRANSITION callback of the Transtion helper class

This callback is called, whenever a Transition is executed.

Parameters

Name Description

self

the TRANSITION object itself

stateMachine

the calling statemachine

Returns

nil, no return value needed.

STATE_MACHINE:INIT

Description

STATE_MACHINE(template)

this is the constructor for the STATE_MACHINE object

Parameters

Name Description

template

this is the template for object creation.
this template should be of the form:
{
    PREPARE= <prepare function>
    CLEANUP= <cleanup function>
    data= {}  — a lua table holding data (can be persisted)
    config= {}, — a lua table holding configuration (not persisted, should not change)
    states= {<STATE>, …​ }, — list of STATE object or lua tables
            -- if lua tables are used, then the STATE objects are created the table as template
    startState= <START>, — the start state, either a STATE object or the name of a STATE (string)
    transitions= {<TRANSITION>, …​} — list of TRANSITION objects or lua tables
            -- if lua tables are used, then the TRANSITION objects are created the table as template
    options= {resetResultsOnPrepare=true} — defines if results should be discarded before PREPARE.
}

Returns

returns the STATE_MACHINE object

Examples

local ESM = require("esi-state-machine")
-- this transition automatically changes from state1 to state2
local esm = ESM({
                  PREPARE= function(self) return "prepare called" end,
                  CLEANUP= function(self) return "cleanup called" end,
                  states = {
                     ESM.STATE({name="state1"}), -- no ON_ENTER, IN_STATE or ON_LEAVE activities
                     ESM.STATE({name="state2"}), -- no ON_ENTER, IN_STATE or ON_LEAVE activities
                  },
                  startState = "state1",
                  transitions = {
                     -- state2 has no outgoing transition, so it is a terminating state
                     ESM.TRANSITION({
                          from= "state1",
                          to =  "state2",
                          CONDITION = function()
                              return true --immediately change from state1 to state2
                          end
                     })
                  }
})
for i=1,10 do
   esm:RUN()
end
return esm.results

STATE_MACHINE:NOP

Description

NOP no operation

does nothing

Parameters

Name Description

_

no parameters needed, but self can be provided

Returns

returns nil

STATE_MACHINE:PREPARE

Description

PREPARE callback e.g. for input processing

The default implementation is setting runTime and triggerTime to now.
If this callback should be overwritte with each instance of the esi-state-machine class,
at least runTime and triggerTime must be set.

Parameters

Name Description

…​

a flexible number of inputs is assumed. This can be used in the overwriting callback as needed.

Returns

returns nil, not return value needed.

STATE_MACHINE:CLEANUP

Description

CLEANUP callback e.g. for ouput generation

This callback is optional and can be used to set ouput parameters or gather debug information etc.

Parameters

Name Description

none

no parameters needed.

Returns

returns nil, not return value needed.

STATE_MACHINE:RUN

Description

STATE_MACHINE:RUN function of the statemachine

For each iterration this function must be called one.
This function will perform at most one state transition.
If a final state (a state with no outgoing transitions) is reached,
then no transitions and no stateHooks are executed and
the currentState, false is returned.
Otherwise:
If resetResultsOnPrepare is set, then all results are cleared.
If .PREPARE is present, this hook is called. Prepare does not store any results.
Then all available transitions are checked for their condition.
On the first match a the transition is triggered.
This calls the OnLeave Hook (and record the results in the state-machine as well as in the state leaft).
Then the OnTransition Hook is called (and record the results in the state-machine).
Then the OnEnter Hook is called ( and records the results in the state-machine, as well as the new state).
After a transition or if no transition took place, the InState Hook is called on the current state.
At the end the CLEANUP Hook is called,
and the current state and "true" is returned
HINT: stateResults are cleared, whenever a state is entered.
HINT: if a transition is taken from state a back to state a,
HINT: this is lead to the same actions as other state transitions:
calling ON_LEAVE, calling ON_TRANSITION and calling ON_ENTER, as well as reseting the stateResults.
If no transition is taken, the StateMachine stays in its current state.

Parameters

Name Description

…​

all inputs to the run methode will be passed on to the PREPARE methode.

Returns

returns the current state ( e.g. after the transition), false (if a final state was reached)

STATE_MACHINE:RESTART

Description

STATE_MACHINE:RESTART restart the state machine

Resets the statemachine to the intial state,
Resets all result entries.
Keeps all data and config entries.
No params needed (apart from self)

Returns

bollean true on success

STATE_MACHINE:TO_MERMAID

Description

STATE_MACHINE:TO_MERMAID create a mermaid diagram from the state machine

Parameters

Name Description

inclDetails

if set, more details are included

exclWildCardTrans

if set, then tranistions to any state (*) are excluded

Returns

string with Mermaid content

STATE_MACHINE:TO_JSON

Description

STATE_MACHINE:TO_JSON convert data and currentState to json

This json can be stored on a variable etc.
This exports the .data section of the statemachine, as well as each state.
This exports the .results section of the statemachine, as well as each state.
This exports the currentState name.
Config settings are not included, as they should be static for most use cases.
No parameters needed (apart from self)

Returns

returns a json string