Packages

State machines, statecharts and workflows for Ecto models.

Current section

Files

Jump to
ex_state_ecto lib ex_state definition compiler.ex
Raw

lib/ex_state/definition/compiler.ex

defmodule ExState.Definition.Compiler do
alias ExState.Definition.{
Chart,
State,
Step,
Transition
}
defmodule Env do
defstruct chart: nil,
macro_env: nil,
virtual_states: %{}
end
def compile(name, body, macro_env) do
env = do_compile(name, body, macro_env)
chart = expand_subject_alias(env.chart, macro_env)
Macro.escape(chart)
end
defp do_compile(name, body, env) do
chart = %Chart{name: name}
env = %Env{chart: chart, macro_env: env}
compile_chart(env, body)
end
defp compile_chart(%Env{} = env, do: body) do
compile_chart(env, body)
end
defp compile_chart(%Env{} = env, {:__block__, _, body}) do
compile_chart(env, body)
end
defp compile_chart(%Env{} = env, body) when is_list(body) do
Enum.reduce(body, env, fn next, acc ->
compile_chart(acc, next)
end)
end
defp compile_chart(%Env{chart: chart} = env, {:subject, _, [name, queryable]}) do
%Env{env | chart: %Chart{chart | subject: {name, queryable}}}
end
defp compile_chart(%Env{chart: chart} = env, {:participant, _, [name]}) do
%Env{env | chart: Chart.put_participant(chart, name)}
end
defp compile_chart(%Env{chart: chart} = env, {:initial_state, _, [id]}) do
%Env{env | chart: %Chart{chart | initial_state: State.name(id)}}
end
defp compile_chart(%Env{virtual_states: virtual_states} = env, {:virtual, _, [name, body]}) do
%Env{env | virtual_states: Map.put(virtual_states, name, body)}
end
defp compile_chart(%Env{} = env, {:state, _, [id]}) do
compile_chart(env, {:state, [], [id, []]})
end
defp compile_chart(%Env{chart: %Chart{states: states} = chart} = env, {:state, _, [id, body]}) do
more_states = compile_state(env, id, body)
merged_states =
Enum.reduce(more_states, states, fn next, acc ->
Map.put(acc, next.name, next)
end)
%Env{env | chart: %Chart{chart | states: merged_states}}
end
defp compile_state(env, id, body) when is_atom(id) do
compile_state(env, nil, id, body)
end
defp compile_state(env, current, id, body) when is_atom(id) do
compile_state(env, current, State.name(id), body)
end
defp compile_state(env, current, name, body) when is_bitstring(name) do
full_name = State.child(current, name)
compile_state(env, full_name, [%State{name: full_name}], body)
end
defp compile_state(env, current, states, do: body) do
compile_state(env, current, states, body)
end
defp compile_state(env, current, states, {:__block__, _, body}) do
compile_state(env, current, states, body)
end
defp compile_state(env, current, [%State{name: name} | _rest] = states, body)
when is_list(body) do
Enum.reduce(body, states, fn
{:state, _, _} = next, [%State{name: ^name} = state | rest] ->
acc = [%State{state | type: :compound} | rest]
compile_state(env, current, acc, next)
next, acc ->
compile_state(env, current, acc, next)
end)
end
defp compile_state(env, current, states, {:using, _, [id]}) do
body = Map.get(env.virtual_states, id, [])
compile_state(env, current, states, body)
end
defp compile_state(env, current, states, {:state, _, [id, body]}) do
next_states = compile_state(env, current, id, body)
next_states ++ states
end
defp compile_state(_env, current, [%State{} = state | rest], {:initial_state, _, [id]}) do
[%State{state | initial_state: State.child(current, id)} | rest]
end
defp compile_state(_env, _current, [state | rest], {:final, _, nil}) do
[%State{state | type: :final} | rest]
end
defp compile_state(
_env,
_current,
[state | rest],
{:parallel, _, [[do: {:__block__, _, body}]]}
) do
steps =
Enum.map(body, fn
{:step, _, [id]} -> Step.new(id, nil)
{:step, _, [id, opts]} -> Step.new(id, Keyword.get(opts, :participant))
end)
[State.add_parallel_steps(state, steps) | rest]
end
defp compile_state(env, current, states, {:step, _, [id]}) do
compile_state(env, current, states, {:step, [], [id, []]})
end
defp compile_state(_env, _current, [state | rest], {:step, _, [id, opts]}) do
repeatable = Keyword.get(opts, :repeatable)
step = Step.new(id, Keyword.get(opts, :participant))
state = State.add_step(state, step)
state =
if repeatable do
State.add_repeatable_step(state, Step.name(id))
else
state
end
[state | rest]
end
defp compile_state(_env, _current, [state | rest], {:repeatable, _, [step_id]}) do
[State.add_repeatable_step(state, Step.name(step_id)) | rest]
end
defp compile_state(_env, _current, [state | rest], {:on_entry, _, [action]}) do
[State.add_action(state, :entry, action) | rest]
end
defp compile_state(_env, _current, [state | rest], {:on_exit, _, [action]}) do
[State.add_action(state, :exit, action) | rest]
end
defp compile_state(env, current, states, {:on_completed, _, [step, target]}) do
compile_state(env, current, states, {:on, [], [{:completed, step}, target]})
end
defp compile_state(env, current, states, {:on_completed, _, [step, target, options]}) do
compile_state(env, current, states, {:on, [], [{:completed, step}, target, options]})
end
defp compile_state(env, current, states, {:on_decision, _, [step, decision, target]}) do
compile_state(env, current, states, {:on, [], [{:decision, step, decision}, target]})
end
defp compile_state(
env,
current,
states,
{:on_decision, _, [step, decision, target, options]}
) do
compile_state(
env,
current,
states,
{:on, [], [{:decision, step, decision}, target, options]}
)
end
defp compile_state(env, current, states, {:on_no_steps, _, [target]}) do
compile_state(env, current, states, {:on_no_steps, [], [target, []]})
end
defp compile_state(env, current, states, {:on_no_steps, _, [target, options]}) do
compile_state(env, current, states, {:on, [], [:__no_steps__, target, options]})
end
defp compile_state(env, current, states, {:on_final, _, [target]}) do
compile_state(env, current, states, {:on_final, [], [target, []]})
end
defp compile_state(env, current, states, {:on_final, _, [target, options]}) do
compile_state(env, current, states, {:on, [], [:__final__, target, options]})
end
defp compile_state(env, current, states, {:on, _, [id, target]}) do
compile_state(env, current, states, {:on, [], [id, target, []]})
end
defp compile_state(_env, current, [state | rest], {:on, _, [event, target, opts]}) do
transition = Transition.new(event, State.resolve(current, target), opts)
[State.add_transition(state, transition) | rest]
end
defp compile_state(_env, _current, states, _) when is_list(states) do
states
end
defp expand_subject_alias(%Chart{subject: {name, queryable}} = chart, env) do
%Chart{chart | subject: {name, Macro.expand(queryable, env)}}
end
defp expand_subject_alias(chart, _env) do
chart
end
end