Packages

State machines, statecharts and workflows for Ecto models.

Current section

Files

Jump to
ex_state_ecto lib ex_state definition state.ex
Raw

lib/ex_state/definition/state.ex

defmodule ExState.Definition.State do
alias ExState.Definition.Step
alias ExState.Definition.Transition
@type state_type :: :atomic | :compound | :final
@type t :: %__MODULE__{
name: String.t(),
type: state_type(),
initial_state: String.t(),
steps: [Step.t()],
ignored_steps: [Step.t()],
repeatable_steps: [String.t()],
transitions: %{required(Transition.event()) => Transition.t()},
actions: %{required(Transition.event()) => atom()}
}
defstruct name: nil,
type: :atomic,
initial_state: nil,
steps: [],
ignored_steps: [],
repeatable_steps: [],
transitions: %{},
actions: %{}
def transition(state, event) do
Map.get(state.transitions, event)
end
def transitions(state) do
state.transitions
|> Map.values()
|> Enum.reduce([], fn transition, events ->
case transition.event do
{:completed, _step} -> events
{:decision, _step, _decision} -> events
event_name when is_atom(event_name) -> [%{event: event_name, state: state.name} | events]
end
end)
end
def actions(state, event) do
Map.get(state.actions, event, [])
end
def add_transition(state, transition) do
%__MODULE__{state | transitions: Map.put(state.transitions, transition.event, transition)}
end
def add_action(state, event, action) do
%__MODULE__{
state
| actions: Map.update(state.actions, event, [action], fn actions -> [action | actions] end)
}
end
def add_step(state, step) do
add_step(state, step, Enum.count(state.steps) + 1)
end
def add_step(state, step, order) do
%__MODULE__{state | steps: [Step.order(step, order) | state.steps]}
end
def add_parallel_steps(state, steps) do
order = Enum.count(state.steps) + 1
Enum.reduce(steps, state, fn step, state ->
add_step(state, step, order)
end)
end
def add_repeatable_step(state, step) do
%__MODULE__{state | repeatable_steps: [step | state.repeatable_steps]}
end
def repeatable?(state, step_name) do
if Enum.member?(state.repeatable_steps, step_name) do
case Enum.find(state.steps ++ state.ignored_steps, fn step -> step.name == step_name end) do
nil ->
true
step ->
step.complete?
end
else
false
end
end
def filter_steps(state, filter) do
{ignored, steps} =
Enum.reduce(state.steps, {[], []}, fn step, {ignored, steps} ->
if filter.(step) do
{ignored, [step | steps]}
else
{[step | ignored], steps}
end
end)
%__MODULE__{state | steps: Enum.reverse(steps), ignored_steps: Enum.reverse(ignored)}
end
def next_steps(state) do
state.steps
|> Enum.filter(fn step -> !step.complete? end)
|> Enum.sort_by(fn step -> step.order end)
|> Enum.chunk_by(fn step -> step.order end)
|> List.first()
end
def complete_step(state, name, decision \\ nil)
def complete_step(state, id, decision) when is_atom(id) do
complete_step(state, Step.name(id), decision)
end
def complete_step(state, name, decision) when is_bitstring(name) do
case next_steps(state) do
nil ->
if repeatable?(state, name) do
{:ok, state}
else
{:error, [], state}
end
next_steps ->
case Enum.any?(next_steps, fn step -> step.name == name end) do
true ->
{:ok, put_completed_step(state, name, decision)}
false ->
if repeatable?(state, name) do
{:ok, state}
else
{:error, next_steps, state}
end
end
end
end
def put_completed_step(state, name, decision \\ nil) when is_bitstring(name) do
steps =
Enum.map(state.steps, fn
%Step{name: ^name} = step -> Step.complete(step, decision)
step -> step
end)
%__MODULE__{state | steps: steps}
end
def final?(%__MODULE__{type: :final}), do: true
def final?(%__MODULE__{}), do: false
def child?(%__MODULE__{} = state, %__MODULE__{} = child_maybe) do
combine(drop_last(child_maybe.name)) == state.name
end
def sibling?(%__MODULE__{} = state, %__MODULE__{} = sibling_maybe) do
combine(drop_last(state.name)) == combine(drop_last(sibling_maybe.name))
end
def name(id) when is_atom(id), do: Atom.to_string(id)
def name(id) when is_bitstring(id), do: id
# The atom may not exist due to being converted to string at compile time.
# Should be safe to use to_atom here since this API shouldn't be
# exposed to external input.
def id(state), do: state.name |> last() |> String.to_atom()
def resolve(nil, next) when is_atom(next), do: next
def resolve(current, next) when is_list(next), do: Enum.map(next, &resolve(current, &1))
def resolve(current, :_), do: current
def resolve(current, {:<, next}) when is_atom(next) do
current
|> parent()
|> sibling(next)
end
def resolve(current, next) when is_atom(next) do
current
|> sibling(next)
end
def parent(nil), do: nil
def parent(state) do
state
|> split()
|> drop_last()
|> combine()
end
def child(nil, state), do: state
def child(current, state) do
current
|> split()
|> append(state)
|> combine()
end
def sibling(nil, state), do: state
def sibling(current, state) do
current
|> split()
|> drop_last()
|> append(state)
|> combine()
end
def drop_last(name) when is_bitstring(name), do: split(name) |> drop_last()
def drop_last(states) when is_list(states), do: Enum.drop(states, -1)
def append(states, state), do: List.insert_at(states, -1, state)
def split(state), do: String.split(state, ".")
def last(state), do: split(state) |> List.last()
def combine(states), do: Enum.join(states, ".")
end