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

## 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 implemented 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_rollback/3` — called when `on_transition/4` resolved to a state that
  is not allowed from the current one, to let the consumer compensate the side effects
  of the rejected transition; receives the event (the atom), the event payload, and
  the whole `t:Finitomata.State.t/0` (with `last_error` set)
- `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 `{:reschedule, 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 payload is cached for fast `state/3` reads (see `Finitomata.StateCache`; the backend defaults to `:ets` and is configurable via `config :finitomata, :cache_backend`) The default value is `true`.

* `:hibernate` - When `true`, the FSM process is hibernated between transitions; pass a `state()` or `[state()]` to hibernate only when the FSM rests in (one of) the given state(s) 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_rollback`
*since 0.41.0* *optional* 

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

This callback will be called when `on_transition/4` succeeded yet resolved to a target
state that is not allowed from the current one, so the transition is rejected and the
FSM stays put.

It lets the consumer compensate (roll back) any side effects performed inside
`on_transition/4` for the now-rejected transition. The `state` carries the original
(pre-transition) payload and a `t:Finitomata.Error.t/0` in `:last_error` whose `:reason`
is `{:not_allowed, from, rejected_to}`. `c:Finitomata.on_failure/3` is still invoked
afterwards.

Because the `:allowed?` guard runs before the persistency and the listener are touched,
a rejected transition is **not** persisted and does **not** notify the listener.

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

> #### Auto-driven transitions reshape the payload {: .info}
>
> For transitions initiated by `Finitomata` itself — the initial entry transition,
> _hard_ (banged, e.g. `start!`) transitions, and `ensure_entry` retries — the
> `event_payload` is normalized into a map carrying a `__retries__` counter
> (a non-map payload `p` becomes `%{payload: p, __retries__: n}`). Handlers matching
> on those events should account for this shape. Payloads passed to a manual
> `transition/4` call are delivered to `on_transition/4` unchanged.

# `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*
