# `Finitomata`
[🔗](https://github.com/am-kantox/finitomata/blob/v0.35.0/lib/finitomata.ex#L5)

## Bird View

`Finitomata` provides a boilerplate for [FSM](https://en.wikipedia.org/wiki/Finite-state_machine) 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](https://plantuml.com/en/state-diagram), [Mermaid](https://mermaid.live), or even custom format. 

> ### Syntax Definition {: .tip}
>
> `Mermaid` **state diagram** format is literally the same as `PlantUML`, so if you want to use it, specify `syntax: :state_diagram` and
> if you want to use **mermaid graph**, specify `syntax: :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}
>
> `Mermaid` does 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` — **mandatory**
- `on_failure/3` — optional
- `on_enter/2` — optional
- `on_exit/2` — optional
- `on_terminate/1` — optional
- `on_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}`.

### Examples

See [examples directory](https://github.com/am-kantox/finitomata/tree/main/examples) for
real-life examples of `Finitomata` usage.

## Example

Let’s define the FSM instance

```elixir
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.

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

> ### `use Finitomata` {: .info}
>
> When you `use Finitomata`, the Finitomata module will
> do the following things for your module:
>
> - set `@behaviour Finitomata`
> - compile and validate _FSM_ declaration, passed as `fsm:` keyword argument
> - turn the module into `GenServer`
> - inject default implementations of optional callbacks specified with
>   `impl_for:` keyword argument (default: `:all`)
> - expose a bunch of functions to query _FSM_ which would be visible in docs
> - leaves `on_transition/4` mandatory callback to be implemeneted by
>   the calling module and injects `before_compile` callback to validate
>   the implementation (this option required `:finitomata` to be included
>   in the list of compilers in `mix.exs`)

## Types of callbacks

Some callbacks in `Finitomata` are pure, others might mutate the inner state (payload).

### Pure callbacks

- `c:Finitomata.on_enter/2` — called when the new state is entered; receives
  the state entered (the atom) and the whole `t:Finitomata.State.t/0`
- `c:Finitomata.on_exit/2` — called when the new state is exited; receives
  the state entered (the atom) and the whole `t:Finitomata.State.t/0`
- `c:Finitomata.on_failure/3` — called when the transition had failed and the target
  state has not been reached; receives the event (the atom), the event payload, and
  the whole `t:Finitomata.State.t/0`
- `c:Finitomata.on_fork/2` — called when the `Finitomata.Fork` has been requested;
  receives the current state (the atom) and the payload `t:Finitomata.State.payload/0`
- `c:Finitomata.on_terminate/1` — called when the finitomata is about to terminate;
  receives the whole `t:Finitomata.State.t/0`

### Mutating callbacks

The following callbacks might (or might not) mutate the inner state (payload).

- `c:Finitomata.on_start/1` — returning anything but `:ignore` amends the inner state
  to the value returned (it might be `{:continue | :ok, Finitomata.State.payload()}`)
- `c:Finitomata.on_timer/2` — when `:ok` or `{:rescedule, non_neg_integer()}` is returned,
  this callback is pure, it’s potentially mutating otherwise
- `c:Finitomata.on_transition/4` — as the main driving callback of the _FSM_, it’s
  definitively mutating, unless errored

## Use with `Telemetría`

`telemetria` library can be used to send all the state changes to the backend,
  configured by this library. To enable metrics sending, one should do the following.

### Add `telemetria` dependency

`telemetria` dependency should be added alongside its backend dependency.
   For `:telemetry` backend, that would be

```elixir
defp deps do
  [
    ...
    {:telemetry, "~> 1.0"},
    {:telemetry_poller, "~> 1.0"},
    {:telemetria, "~> 0.22"}
  ]
```

### Configure `telemetria` library in a compile-time config

```elixir
config :telemetria,
  backend: Telemetria.Backend.Telemetry,
  purge_level: :debug,
  level: :info,
```

### Add `:telemetria` compiler
`:telemetria` compiler should be added to the list of `mix` compilers, alongside
  `:finitomata` compiler.

```elixir
def project do
  [
    ...
    compilers: [:finitomata, :telemetria | Mix.compilers()],
    ...
  ]
end
```

### Configure `:finitomata` to use `:telemetria`

The configuration parameter `[:finitomata, :telemetria]` accepts the following values:

- `false` — `:telemetria` metrics won’t be sent
- `true` — `:telemetria` metrics will be send for _all_ the callbacks
- `[callback, ...]` — `:telemetria` metrics will be send for the specified callbacks

Available callbacks may be seen below in this module documentation. Please note,
  that the events names would be `event: [__MODULE__, :safe_on_transition]` and like.

```elixir
config :finitomata, :telemetria, true
```

See [`telemetria`](https://hexdocs.pm/telemetria) docs for further config details.

## Options to `use Finitomata`

* `:fsm` (`t:String.t/0`) - Required. The FSM declaration with the syntax defined by `syntax` option.

* `:forks` (list of tuple of `t:atom/0`,  values) - The keyword list of states and modules where the FSM forks and awaits for another process to finish The default value is `[]`.

* `:syntax` - The FSM dialect parser to convert the declaration to internal FSM representation. The default value is `:flowchart`.

* `:impl_for` - The list of transitions to inject default implementation for. The default value is `:all`.

* `:telemetria_levels` - The telemetría level for selected callbacks The default value is `[all: :info, on_enter: :debug, on_exit: :debug, on_failure: :warning, on_timer: :debug]`.

* `:timer` - The interval to call `on_timer/2` recurrent event. The default value is `false`.

* `:auto_terminate` - When `true`, the transition to the end state is initiated automatically. The default value is `false`.

* `:cache_state` (`t:boolean/0`) - When `true`, the FSM state is cached in `:persistent_term` The default value is `true`.

* `:hibernate` - When `true`, the FSM process is hibernated between transitions The default value is `false`.

* `:ensure_entry` - The list of states to retry transition to until succeeded. The default value is `[]`.

* `:shutdown` (`t:pos_integer/0`) - The shutdown interval for the `GenServer` behind the FSM. The default value is `5000`.

* `:persistency` - The implementation of `Finitomata.Persistency` behaviour to backup FSM with a persistent storage. The default value is `nil`.

* `:listener` - The implementation of `Finitomata.Listener` behaviour _or_ a `GenServer.name()` to receive notification after transitions. The default value is `nil`.

* `:mox_envs` - The list of environments to implement `mox` listener for The default value is `[:test, :finitomata]`.

# `event_payload`

```elixir
@type event_payload() :: term()
```

The payload that can be passed to each call to `transition/3`

# `flow_implementation`

```elixir
@type flow_implementation() :: module()
```

The implementation of the Flow (basically, the module having `use Finitomata.Flow` clause)

# `fork_resolution`

```elixir
@type fork_resolution() :: {:ok, flow_implementation()} | :ok
```

The resolution of fork

# `fsm_name`

```elixir
@type fsm_name() :: any()
```

The name of the FSM (might be any term, but it must be unique)

# `id`

```elixir
@type id() :: any()
```

The ID of the `Finitomata` supervision tree, useful for the concurrent
  using of different `Finitomata` supervision trees.

# `implementation`

```elixir
@type implementation() :: module()
```

The implementation of the FSM (basically, the module having `use Finitomata` clause)

# `payload`

```elixir
@type payload() :: term()
```

The payload that is carried by `Finitomata` instance, returned by `Finitomata.state/2`

# `transition_resolution`

```elixir
@type transition_resolution() ::
  {:ok, Finitomata.Transition.state(), Finitomata.State.payload()}
  | {:error, any()}
```

The resolution of transition, when `{:error, _}` tuple, the transition is aborted

# `validation_error`

```elixir
@type validation_error() ::
  :initial_state | :final_state | :orphan_from_state | :orphan_to_state
```

Error types of FSM validation

# `on_enter`
*optional* 

```elixir
@callback on_enter(
  current_state :: Finitomata.Transition.state(),
  state :: Finitomata.State.t()
) :: :ok
```

This callback will be called on entering the state.

# `on_exit`
*optional* 

```elixir
@callback on_exit(
  current_state :: Finitomata.Transition.state(),
  state :: Finitomata.State.t()
) :: :ok
```

This callback will be called on exiting the state.

# `on_failure`
*optional* 

```elixir
@callback on_failure(
  event :: Finitomata.Transition.event(),
  event_payload :: event_payload(),
  state :: Finitomata.State.t()
) :: :ok
```

This callback will be called if the transition failed to complete to allow
the consumer to take an action upon failure.

# `on_fork`
*optional* 

```elixir
@callback on_fork(
  current_state :: Finitomata.Transition.state(),
  state_payload :: Finitomata.State.payload()
) :: fork_resolution()
```

This callback will be called when the transition processor encounters fork state.

# `on_start`
*optional* 

```elixir
@callback on_start(state :: Finitomata.State.payload()) ::
  {:continue, Finitomata.State.payload()}
  | {:ok, Finitomata.State.payload()}
  | :ignore
  | :ok
```

This callback will be called from the underlying `c:GenServer.init/1`.

Unlike other callbacks, this one might raise preventing the whole FSM from start.

When `:ok`, `:ignore`, or `{:continue, new_payload}` tuple is returned from the callback,
   the normal initalization continues through continuing to the next state.

`{:ok, new_payload}` prevents the _FSM_ from automatically getting into start state,
  and the respective transition must be called manually.

# `on_terminate`
*optional* 

```elixir
@callback on_terminate(state :: Finitomata.State.t()) :: :ok
```

This callback will be called on transition to the final state to allow
the consumer to perform some cleanup, or like.

# `on_timer`
*optional* 

```elixir
@callback on_timer(
  current_state :: Finitomata.Transition.state(),
  state :: Finitomata.State.t()
) ::
  :ok
  | {:ok, Finitomata.State.payload()}
  | {:transition, {Finitomata.Transition.event(), event_payload()},
     Finitomata.State.payload()}
  | {:transition, Finitomata.Transition.event(), Finitomata.State.payload()}
  | {:reschedule, non_neg_integer()}
```

This callback will be called recurrently if `timer: pos_integer()`
  option has been given to `use Finitomata`.

By design, `Finitomata` library is the in-memory solution (unless `persistency: true`
is set in options _and_ the persistency layer is implemented by the consumer’s code.)

That being said, the consumer should not rely on `on_timer/2` consistency between restarts.

# `on_transition`

```elixir
@callback on_transition(
  current_state :: Finitomata.Transition.state(),
  event :: Finitomata.Transition.event(),
  event_payload :: event_payload(),
  state_payload :: Finitomata.State.payload()
) :: transition_resolution()
```

This callback will be called from each transition processor.

# `alive?`

Returns `true` if the _FSM_ specified is alive, `false` otherwise.

# `allowed?`

```elixir
@spec allowed?(id(), fsm_name(), Finitomata.Transition.state()) :: boolean()
```

Returns `true` if the transition to the state `state` is possible, `false` otherwise.

# `finitomata_id`

```elixir
@spec finitomata_id(Finitomata.State.t()) :: id()
```

Returns an `id` of the finitomata instance the FSM runs on

# `fqn`

```elixir
@spec fqn(id(), fsm_name()) :: {:via, module(), {module(), any()}}
```

Fully qualified name of the _FSM_ backed by `Finitonata`

# `fsm_name`

```elixir
@spec fsm_name(Finitomata.State.t()) :: fsm_name()
```

Returns a plain version of the FSM name as it has been passed to `start_fsm/4`

# `lookup`

```elixir
@spec lookup(id(), fsm_name()) :: pid() | nil
```

Fast check to validate the FSM process with such `id` and `target` exists.

The arguments are

- the id of the FSM (optional)
- the name of the FSM

# `match_state?`

```elixir
@spec match_state?(
  matched :: Finitomata.Transition.state(),
  state ::
    Finitomata.Transition.state()
    | {Finitomata.Transition.state(), pos_integer()}
) :: boolean()
```

Helper to match finitomata state from history, which can be `:state`, or `{:state, reenters}`

# `pid`

```elixir
@spec pid(Finitomata.State.t()) :: pid() | nil
```

Looks up and returns the PID of the FSM by the `State.t()`.

# `pid`

```elixir
@spec pid(id(), fsm_name()) :: pid() | nil
```

Looks up and returns the PID of the FSM by the `State.t()`.

# `responds?`

```elixir
@spec responds?(id(), fsm_name(), Finitomata.Transition.event()) :: boolean()
```

Returns `true` if the transition by the event `event` is possible, `false` otherwise.

# `start_fsm`

Starts the FSM instance.

The arguments are

- the global name of `Finitomata` instance (optional, defaults to `Finitomata`)
- the name of the FSM (might be any term, but it must be unique)
- the implementation of FSM (the module, having `use Finitomata`)
- the payload to be carried in the FSM state during the lifecycle

Before `v0.15.0` the second and third parameters were expected in different order.
This is deprecated and will be removed in `v1.0.0`.

The FSM is started supervised. If the global name/id is given, it should be passed
  to all calls like `transition/4`

# `state`

The state of the FSM.

The arguments are

- the id of the FSM (optional)
- the name of the FSM
- defines whether the cached state might be returned or should be reloaded

# `sup_alive?`

```elixir
@spec sup_alive?(id()) :: boolean()
```

Returns `true` if the supervision tree is alive, `false` otherwise.

# `sup_tree`

```elixir
@spec sup_tree(id()) :: [
  supervisor: nil | pid(),
  manager: nil | pid(),
  registry: nil | pid()
]
```

Returns supervision tree of `Finitomata`. The healthy tree has all three `pid`s.

# `transition`

Initiates the transition.

The arguments are

- the id of the FSM (optional)
- the name of the FSM
- `event` atom or `{event, event_payload}` tuple; the payload will be passed to the respective
  `on_transition/4` call, payload is `nil` by default
- `delay` (optional) the interval in milliseconds to apply transition after

---

*Consult [api-reference.md](api-reference.md) for complete listing*
