Current section

Files

Jump to
finitomata lib accessible.ex
Raw

lib/accessible.ex

defmodule Finitomata.Accessible do
@moduledoc since: "0.26.0"
@moduledoc """
The convenience module, exposing `start_link/1` to embed `Finitomata.Supervisor`
implementation into a supervision tree in an agnostic way.
This module implements `Access` in a contrived way, allowing to deal with underlying
_FSM_ instances in a following way.
```elixir
iex|🌢|1 ▶ Finitomata.Accessible.start_link(type: Finitomata, implementation: L, id: L1)
{:ok, #PID<0.214.0>}
iex|🌢|2 ▶ sup = Finitomata.Accessible.lookup(L1)
%Finitomata.Accessible{
type: Finitomata,
id: L1,
implementation: Finitomata.Test.Log,
last_event: nil,
cached_pid: #PID<0.214.0>
}
iex|🌢|3put_in(sup, ["MyFSM"], :accept)
07:16:34.736 [debug] [→ ↹] []
07:16:34.738 [debug] [✓ ⇄] with: [current: :*, event: :__start__, event_payload: %{payload: nil, __retries__: 1}, state: %{}]
07:16:34.738 [debug] [← ↹] []
07:16:34.738 [debug] [→ ↹] []
07:16:34.739 [debug] [✓ ⇄] with: [current: :idle, event: :accept, event_payload: nil, state: %{}]
07:16:34.739 [debug] [← ↹] []
%Finitomata.Accessible{
type: Finitomata,
id: L1,
implementation: Finitomata.Test.Log,
last_event: {"MyFSM", :accept},
cached_pid: #PID<0.214.0>
}
iex|🌢|4get_in(sup, ["MyFSM"])
#Finitomata<[
name: {Finitomata.L1.Registry, "MyFSM"},
state: [current: :accepted, previous: :idle, payload: %{}],
internals: [errored?: false, persisted?: false, timer: false]
]>
```
"""
schema = [
type: [
required: false,
default: Infinitomata,
type: {:custom, Finitomata, :behaviour, [Finitomata.Supervisor]},
doc:
"The actual `Finitomata.Supervisor` implementation (typically, `Finitomata` or `Infinitomata`)"
],
implementation: [
required: true,
type: :atom,
doc: "The actual implementation of _FSM_ (the module, having `use Finitomata` clause)"
],
id: [
required: false,
default: nil,
type: :any,
doc:
"The unique `ID` of this _Finitomata_ “branch,” when `nil` the `implementation` value would be used"
]
]
@schema NimbleOptions.new!(schema)
@doc """
Supervision tree embedder.
## Options to `Finitomata.Accessible.start_link/1`
#{NimbleOptions.docs(@schema)}
"""
def start_link(opts \\ []) do
opts = NimbleOptions.validate!(opts, @schema)
type = Keyword.fetch!(opts, :type)
implementation = Keyword.fetch!(opts, :implementation)
id = Keyword.fetch!(opts, :id)
on_start = type.start_link(id)
case on_start do
{:ok, pid} ->
:persistent_term.put(
{__MODULE__, id},
struct!(__MODULE__, type: type, id: id, implementation: implementation, cached_pid: pid)
)
{:error, {:already_started, pid}} ->
:persistent_term.put(
{__MODULE__, id},
struct!(__MODULE__, type: type, id: id, implementation: implementation, cached_pid: pid)
)
_ ->
nil
end
on_start
end
@doc """
Looks up the `Finitomata.Accessible` instance for the given `id`
"""
@spec lookup(Finitomata.id()) :: t()
def lookup(id), do: :persistent_term.get({__MODULE__, id}, nil)
@typedoc "The convenience struct representing the `Finitomata.Supervisor` with `Access`"
@type t :: %{
__struct__: __MODULE__,
type: Finitomata.Supervisor.t(),
id: Finitomata.id(),
implementation: module(),
last_event: nil | {Finitomata.fsm_name(), Finitomata.event_payload()},
cached_pid: nil | pid()
}
defstruct [:type, :id, :implementation, :last_event, :cached_pid]
@behaviour Access
@doc false
@impl Access
def fetch(%{__struct__: __MODULE__, type: type, id: id}, fsm_name) do
case type.state(id, fsm_name, :full) do
%Finitomata.State{} = state -> {:ok, state}
_ -> :error
end
end
@doc false
@impl Access
def pop(%{__struct__: __MODULE__} = data, _key),
do: {nil, data}
@doc false
@impl Access
def get_and_update(%{__struct__: __MODULE__, type: type, id: id} = data, fsm_name, function) do
_ =
if not type.alive?(id, fsm_name) do
{:ok, _pid} = type.start_fsm(id, fsm_name, data.implementation, %{})
end
state = type.state(id, fsm_name, :full)
case function.(state) do
:pop ->
{state, data}
{_state, event_payload} ->
:ok = type.transition(id, fsm_name, event_payload)
{state, %{data | last_event: {fsm_name, event_payload}}}
end
end
end