finitomata
finitomata copied to clipboard
FSM implementation generated from Mermaid/PlantUML textual representation
Finitomata

The FSM boilerplate based on callbacks
Bird View
Finitomata provides a boilerplate for FSM implementation, allowing to concentrate on the business logic rather than on the process management and transitions/events consistency tweaking.
It reads a description of the FSM from a string in PlantUML, Mermaid, or even custom format.
Syntax Definition {: .tip}
Mermaidstate diagram format is literally the same asPlantUML, so if you want to use it, specifysyntax: :state_diagramand if you want to use mermaid graph, specifysyntax: :flowchart. The latter is the default.
Basically, it looks more or less like this
PlantUML / :state_diagram
[*] --> s1 : to_s1
s1 --> s2 : to_s2
s1 --> s3 : to_s3
s2 --> [*] : ok
s3 --> [*] : ok
Mermaid / :flowchart
s1 --> |to_s2| s2
s1 --> |to_s3| s3
Using
syntax: :flowchart{: .tip}
Mermaiddoes not allow to explicitly specify transitions (and hence event names) from the starting state and to the end state(s), these states names are implicitly set to:*and events to:__start__and:__end__respectively.
Finitomata validates the FSM is consistent, namely it has a single initial state, one or more final states, and no orphan states. If everything is OK, it generates a GenServer that could be used both alone, and with provided supervision tree. This GenServer requires to implement six callbacks
on_transition/4— mandatoryon_failure/3— optionalon_enter/2— optionalon_exit/2— optionalon_terminate/1— optionalon_timer/2— optional
All the callbacks do have a default implementation, that would perfectly handle transitions having a single to state and not requiring any additional business logic attached.
Upon start, it moves to the next to initial state and sits there awaiting for the transition request. Then it would call an on_transition/4 callback and move to the next state, or remain in the current one, according to the response.
Upon reaching a final state, it would terminate itself. The process keeps all the history of states it went through, and might have a payload in its state.
Special Events
If the event name is ended with a bang (e. g. idle --> |start!| started) and
this event is the only one allowed from this state (there might be several transitions though,)
it’d be considered as determined and FSM will be transitioned into the new state instantly.
If the event name is ended with a question mark (e. g. idle --> |start?| started,)
the transition is considered as expected to fail; no on_failure/2 callback would
be called on failure and no log warning will be printed.
FSM Tuning and Configuration
Recurrent Callback
If timer: non_neg_integer() option is passed to use Finitomata,
then c:Finitomata.on_timer/2 callback will be executed recurrently.
This might be helpful if FSM needs to update its state from the outside
world on regular basis.
Automatic FSM Termination
If auto_terminate: true() | state() | [state()] option is passed to use Finitomata,
the special __end__ event to transition to the end state will be called automatically
under the hood, if the current state is either listed explicitly, or if the value of
the parameter is true.
Ensuring State Entry
If ensure_entry: true() | [state()] option is passed to use Finitomata, the transition
attempt will be retried with {:continue, {:transition, {event(), event_payload()}}} message
until succeeded. Neither on_failure/2 callback is called nor warning message is logged.
The payload would be updated to hold __retries__: pos_integer() key. If the payload was not a map,
it will be converted to a map %{payload: payload}.
Example
Let’s define the FSM instance
defmodule MyFSM do
@fsm """
s1 --> |to_s2| s2
s1 --> |to_s3| s3
"""
use Finitomata, fsm: @fsm, syntax: :flowchart
## or uncomment lines below for `:state_diagram` syntax
# @fsm """
# [*] --> s1 : to_s1
# s1 --> s2 : to_s2
# s1 --> s3 : to_s3
# s2 --> [*] : __end__
# s3 --> [*] : __end__
# """
# use Finitomata, fsm: @fsm, syntax: :state_diagram
@impl Finitomata
def on_transition(:s1, :to_s2, _event_payload, state_payload),
do: {:ok, :s2, state_payload}
end
Now we can play with it a bit.
# or embed into supervision tree using `Finitomata.child_spec()`
{:ok, _pid} = Finitomata.start_link()
Finitomata.start_fsm MyFSM, "My first FSM", %{foo: :bar}
Finitomata.transition "My first FSM", {:to_s2, nil}
Finitomata.state "My first FSM"
#⇒ %Finitomata.State{current: :s2, history: [:s1], payload: %{foo: :bar}}
Finitomata.allowed? "My first FSM", :* # state
#⇒ true
Finitomata.responds? "My first FSM", :to_s2 # event
#⇒ false
Finitomata.transition "My first FSM", {:__end__, nil} # to final state
#⇒ [info] [◉ ⇄] [state: %Finitomata.State{current: :s2, history: [:s1], payload: %{foo: :bar}}]
Finitomata.alive? "My first FSM"
#⇒ false
Typically, one would implement all the on_transition/4 handlers, pattern matching on the state/event.
Installation
def deps do
[
{:finitomata, "~> 0.20"}
]
end
Changelog
0.22.0— [FIX]Infinitomata.start_fsm/4is finally 102% sync0.21.4— [FIX]Finitomata.Poolinitialization in cluster0.21.3— [FIX] proper return fromInfinitomata.start_fsm/40.21.1— [UPD]listener: :moxand betterFinitomata.ExUnitdocs0.20.2— [UPD] allow guard matches in the RHO of~>operator inassert_transition/30.20.0— [FIX] starting pool on distribution, re-synch on:badrpcfailure0.19.0— [UPD]Finitomata.ExUnitlighten options check (compile-time module dependencies suck in >=1.16)0.18.0— [UPD] asynchronousFinitomata.Poolon top ofInfinitomata0.17.0— [UPD] careful naming andFinitomata.Throttler0.16.0— [UPD]Infinitomataas a self-contained distributed implementation leveraging:pg0.15.0— [UPD] support snippet formatting for modern Elixir0.14.6— [FIX] persistency flaw when loading [credits @peaceful-james]0.14.5— [FIX]require LoggerinHook0.14.4— [FIX] Docs cleanup (credits: @TwistingTwists),PlantUMLproper entry0.14.3— [FIX] Draw diagram in docs0.14.2— [FIX] StopEventsprocess0.14.1— [FIX] Incorrect detection of superfluous determined transitions0.14.0—Finitomata.ExUnitimprovements0.13.0— compile-time helpers for FSM,Finitomata.ExUnit0.12.1—c:Finitomata.on_start/1callback0.11.3— [FIX] better error message for options (credits @ray-sh)0.11.2— [DEBT] exportedFinitomata.fqn/20.11.1—Inspect,:flowchart/:state_diagramas default parsers, behaviourParser0.11.0—{:ok, state_payload}return fromon_timer/2,:persistent_termto cache state0.10.0— support for several supervision trees withids, experimental support for persistence scaffold0.9.0— [FIX] malformed callbacks had the FSM broken0.8.2— last error is now kept in the state (credits to @egidijusz)0.8.1— improvements to:finitomatacompiler0.8.0—:finitomatacompiler to warn/hint about not implemented ambiguous transitions0.7.2— [FIX]banged!transitions must not be determined0.6.3—soft?events which do not callon_failure/2and do not log errors0.6.2—ensure_entry:option to retry a transition0.6.1— code cleanup +auto_terminate:option to make:__end__transition imminent0.6.0—on_timer/2and banged imminent transitions0.5.2—state()type on generated FSMs0.5.1— fixed specs [credits @egidijusz]0.5.0— all callbacks buton_transition/4are optional, acceptimpl_for:param touse Finitomata0.4.0— allow anonymous FSM instances0.3.0—en_entry/2andon_exit/2optional callbacks0.2.0— Mermaid support