Packages

Provides DSL and functions for defining and handling `Ecto.Schema` based FSM

Current section

Files

Jump to
ecto_fsm lib ecto fsm machine.ex
Raw

lib/ecto/fsm/machine.ex

defmodule Ecto.FSM.Machine do
@moduledoc """
Provides functions for using FSMs.
* `Ecto.FSM.Machine.fsm/1` merge fsm from multiple handlers (see
`Ecto.FSM` to see how to define one).
* `Ecto.FSM.Machine.event_bypasses/1` merge bypasses from multiple
handlers (see `Ecto.FSM` to see how to define one).
* `Ecto.FSM.Machine.event/2` allows you to execute the correct
handler from a state and action
"""
alias Ecto.FSM.Machine.State
@type meta_event_error :: :illegal_action | term
@type meta_event_reply :: {:ok, State.t()} | {:error, meta_event_error}
@doc """
Returns `Ecto.FSM.specs()` built from all handlers
"""
@spec fsm(State.t()) :: Ecto.FSM.specs()
def fsm(state), do: do_fsm(State.handlers(state))
@doc """
Returns global bypasses
"""
@spec event_bypasses(State.t()) :: Ecto.FSM.bypasses()
def event_bypasses(state), do: do_event_bypasses(State.handlers(state))
@doc """
Returns handler for given action, if any
"""
@spec find_handler({State.t(), Ecto.FSM.trans()}) :: Ecto.FSM.handler() | nil
def find_handler({state, trans}) do
{State.state_name(state), trans}
|> do_find_handler(State.handlers(state))
end
@doc """
Find bypass
"""
@spec find_bypass_handler(State.t(), Ecto.FSM.trans()) :: Ecto.FSM.handler() | nil
def find_bypass_handler(state, trans) do
event_bypasses(state)[trans]
end
@doc """
Returns global doc
"""
@spec infos(State.t(), Ecto.FSM.trans()) :: Ecto.FSM.docs()
def infos(state, action) do
state
|> State.handlers()
|> do_infos(action)
end
@doc """
Returns info for particular transition
"""
@spec find_info(Ecto.FSM.state(), Ecto.FSM.trans()) :: Ecto.FSM.info() | nil
def find_info(state, trans) do
docs = infos(state, trans)
docs
|> Map.get({:transition_doc, State.state_name(state), trans})
|> case do
nil ->
find_bypass_info(docs, trans)
doc ->
{:transition, doc}
end
end
@doc """
Returns available actions
"""
@spec available_actions(State.t()) :: [Ecto.FSM.trans()]
def available_actions(state) do
fsm_actions =
state
|> Ecto.FSM.Machine.fsm()
|> Enum.filter(fn {{from, _}, _} -> from == State.state_name(state) end)
|> Enum.map(fn {{_, action}, _} -> action end)
bypasses_actions =
state
|> Ecto.FSM.Machine.event_bypasses()
|> Map.keys()
Enum.uniq(fsm_actions ++ bypasses_actions)
end
@doc """
Returns true if given action is available
"""
@spec action_available?(State.t(), Ecto.FSM.trans()) :: boolean
def action_available?(state, action) do
actions = available_actions(state)
if :_ in actions do
true
else
action in available_actions(state)
end
end
@doc """
Meta application of the transition function, using `find_handler/2`
to find the module implementing it.
"""
@spec event(State.t(), {Ecto.FSM.trans(), term}) :: meta_event_reply
def event(state, {trans, params}) do
{state, trans}
|> find_handler()
|> case do
nil ->
do_find_bypass(state, trans, params)
handler ->
do_apply_event(handler, state, trans, params)
end
end
###
### Priv
###
defp do_fsm(handlers) when is_list(handlers) do
handlers
|> Enum.map(& &1.fsm)
|> Enum.concat()
|> Enum.into(%{})
end
defp do_event_bypasses(handlers) when is_list(handlers),
do: handlers |> Enum.map(& &1.event_bypasses) |> Enum.concat() |> Enum.into(%{})
defp do_infos(handlers, _trans) when is_list(handlers) do
handlers
|> Enum.map(& &1.docs)
|> Enum.concat()
|> Enum.into(%{})
end
defp do_find_bypass(state, trans, params) do
state
|> find_bypass_handler(trans)
|> case do
nil ->
{:error, :illegal_action}
handler ->
do_apply_bypass(handler, state, trans, params)
end
end
defp do_find_handler({state_name, trans}, handlers) when is_list(handlers) do
handlers
|> do_fsm()
|> Map.get({state_name, trans})
|> case do
{handler, _} -> handler
_ -> nil
end
end
defp do_apply_bypass(handler, state, action, params) do
res =
try do
apply(handler, action, [params, state])
rescue
FunctionClauseError ->
{:error, :illegal_action}
end
case res do
{:keep_state, state} ->
{:ok, state}
{:next_state, state_name, state} ->
{:ok, State.set_state_name(state, state_name)}
{:error, _} = e ->
e
_other ->
orig = State.state_name(state)
raise Ecto.FSM.Error, handler: handler, statename: orig, action: action
end
end
defp do_apply_event(handler, state, action, params) do
orig = State.state_name(state)
res =
try do
apply(handler, orig, [{action, params}, state])
rescue
FunctionClauseError ->
{:error, :illegal_action}
end
case res do
{:next_state, state_name, state} ->
{:ok, State.set_state_name(state, state_name)}
{:error, _} = e ->
e
_other ->
raise Ecto.FSM.Error, handler: handler, statename: orig, action: action
end
end
defp find_bypass_info(docs, action) do
docs
|> Map.get({:event_doc, action})
|> case do
nil ->
nil
doc ->
{:bypass, doc}
end
end
end