A scriptable FSM library for Go
- Tengo language: fast and secure
- Scriptable functions: transition conditions, transition actions, state entry and exit actions
- Immutable values
A state machine is defined by
- A set of named states, and,
- An ordered list of transitions between states
A state is defined by:
- Name: a unique string identifier in the state machine
- Entry Action: a function that's executed when entering the state
- Exit Action: a function that's executed when exiting the state
A transition is defined by:
- Src: the current state
- Dst: the next state
- Condition: a function that evaluates the condition
- Action: a function that's executed when the condition is fulfilled
If condition function name is not specified (an empty space), the transition is considered as unconditional (always evalutes to true).
Condition functions in the script should take 3 arguments:
func(src, dst, v) {
/* some logic */
return some_value
}
src
: the current statedst
: the next statev
: the data value (immutable)
The state machine use the returned value to determine the condition of the transition. E.g. condition is fulfilled if the value is truthy. In Tengo, the function that does not return anything is treated as if it returns undefined
which is falsy.
Action functions in the script should take 3 arguments:
func(src, dst, v) {
/* some logic */
return some_value
}
src
: the current statedst
: the next statev
: the data value (immutable)
The data value passed to action functions is immutable, but, the function may return a new value to change the data value for the future condition/action functions.
- If the function returns
undefined
(or does not return anything), the data value remains unmodified. - If the function returns
error
objects (e.g.return error("some error")
), the state machine stops and returns an error fromStateMachine.Run
function call. - If the function returns a value of any other type, the data value of the state machine is changed to the returned value.
When running the state machine, user can pass an input data value that will be used by condition and action functions. The state machine will return the final output data value when there are no more transitions available.
- When the state machine starts, it's given an initial state and the input data.
- The state machine evaluates a list of transitions that are defined with the current state as its
src
state. The state machine evaluates the transitions in the same order they were added (defined).- If
condition
script is specified, the state machine runs the script to determines whether the condition is fulfilled or not. - If
condition
script is not specified,
- If
- If one of the transition's condition is fulfilled, the state machine runs the action scripts:
- It runs
exit action
of the current state if it's defined. - It runs
action
of the transition if it's defined. - It runs
entry action
of the next state if it's defined.
- It runs
- If no transitions were fulfilled, the state machine stops and returns the final value.
- Repeat from the step 2.
Here's an example code for an FSM that tests if the input string is valid decimal numbers (e.g. 123.456
) or not:
package main
import (
"fmt"
"github.com/d5/go-fsm"
)
var decimalsScript = []byte(`
fmt := import("fmt")
export {
// test if the first character is a digit
is_digit: func(src, dst, v) {
return v[0] >= '0' && v[0] <= '9'
},
// test if the first character is a period
is_dot: func(src, dst, v) {
return v[0] == '.'
},
// test if there are no more characters left
is_eol: func(src, dst, v) {
return len(v) == 0
},
// prints out transition info
print_tx: func(src, dst, v) {
fmt.printf("%s -> %s: %q\n", src, dst, v)
},
// cut the first character
enter: func(src, dst, v) {
return v[1:]
},
enter_end: func(src, dst, v) {
return "valid number"
},
enter_error: func(src, dst, v) {
return "invalid number: " + v
}
}`)
func main() {
// build and compile state machine
machine, err := fsm.New(decimalsScript).
State("S", "enter", ""). // start
State("N", "enter", ""). // whole numbers
State("P", "enter", ""). // decimal point
State("F", "enter", ""). // fractional part
State("E", "enter_end", ""). // end
State("X", "enter_error", ""). // error
Transition("S", "E", "is_eol", "print_tx").
Transition("S", "N", "is_digit", "print_tx").
Transition("S", "X", "", "print_tx").
Transition("N", "E", "is_eol", "print_tx").
Transition("N", "N", "is_digit", "print_tx").
Transition("N", "P", "is_dot", "print_tx").
Transition("N", "X", "", "print_tx").
Transition("P", "F", "is_digit", "print_tx").
Transition("P", "X", "", "print_tx").
Transition("F", "E", "is_eol", "print_tx").
Transition("F", "F", "is_digit", "print_tx").
Transition("F", "X", "", "print_tx").
Compile()
if err != nil {
panic(err)
}
// test case 1: "123.456"
res, err := machine.Run("S", "123.456")
if err != nil {
panic(err)
}
fmt.Println(res)
// test case 2: "12.34.65"
res, err = machine.Run("S", "12.34.56")
if err != nil {
panic(err)
}
fmt.Println(res)
}