A scriptable FSM library for Go
A scriptable FSM library for Go
A state machine is defined by
A state is defined by:
A transition is defined by:
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.
undefined
(or does not return anything), the data value remains unmodified.error
objects (e.g. return error("some error")
), the state machine stops and returns an error from StateMachine.Run
function call.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.
src
state. The state machine evaluates the transitions in the same order they were added (defined).
condition
script is specified, the state machine runs the script to determines whether the condition is fulfilled or not.condition
script is not specified,exit action
of the current state if it's defined.action
of the transition if it's defined.entry action
of the next state if it's defined.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)
}