Current section

Files

Jump to
lab42_simple_state_machine lib lab42 state_machine runner.ex
Raw

lib/lab42/state_machine/runner.ex

defmodule Lab42.StateMachine.Runner do
use Lab42.StateMachine.Types
@moduledoc """
Runs the state machine by finding and executing transitions
"""
@doc false
@spec run( state_t(), list(), list(), any(), transition_map_t() ) :: result_t()
def run(current_state, input, result, data, state_definitions)
def run(current_state, [], result, data, _), do: {current_state, Enum.reverse(result), data}
def run(current_state, input, result, data, states) do
# insp {:run, current_state, input, result, data}
case Map.get(states, current_state) do
nil -> {:error, "No transitions found for current state", current_state}
transitions -> _run_transitions(current_state, transitions, input, result, data, states)
end
end
@doc """
Convenience transformer function to stop the state machine, can be used with the atom `:halt`
"""
@spec halt_transfo( any() ) :: :halt
def halt_transfo(_), do: :halt
@doc """
Convenience function to push an input to the output without changing it, can be used with the atom `:id` in
the transformer position of a transition, e.g. `{~r{alpha}, :id, fn count, _ -> count + 1 end}`
"""
@spec ident_transfo( match_t() ) :: any()
def ident_transfo({_, line}), do: line
@doc """
Convenience function to not change the data. It can be used with the atom `:id` in
the updater position of a transition, e.g. `{~r{alpha}, fn {_, line} -> String.reverse(line), :id}`
"""
@spec ident_updater( any(), any() ) :: any()
def ident_updater(data, _), do: data
@doc """
Convenience function to ignore an input, it can be used with the atom `:ignore` in
the transformer position of a transition, e.g. `{~r{alpha}, :ignore, fn count, _ -> count + 1 end}`
"""
@spec ignore_input( any() ) :: :ignore
def ignore_input(_), do: :ignore
@spec _execute_transition( transition_t(), any(), any(), any() ) :: {state_t(), any(), any()}
defp _execute_transition(transition, matches, input, data)
defp _execute_transition({_, transformer, updater, new_state}, matches, input, data) do
transformed = transformer.({matches, input})
updated = updater.(data, {matches, input})
{new_state, transformed, updated}
end
@spec _match_trigger( trigger_t(), any() ) :: any()
defp _match_trigger(trigger, input)
defp _match_trigger(true, _), do: []
defp _match_trigger(fn_trigger, input) when is_function(fn_trigger), do: fn_trigger.(input)
defp _match_trigger(rgx_trigger, input), do: Regex.run(rgx_trigger, input)
@spec _normalize_transition( incomplete_transition_t(), state_t() ) :: transition_t()
defp _normalize_transition(transition, current_state)
defp _normalize_transition({trigger}, current_state), do: {trigger, &ident_transfo/1, &ident_updater/2, current_state}
defp _normalize_transition({trigger, f1}, current_state), do: _replace_symbolic_fns({trigger, f1, &ident_updater/2, current_state})
defp _normalize_transition({trigger, f1, f2}, current_state), do: _replace_symbolic_fns({trigger, f1, f2, current_state})
defp _normalize_transition(already_normalized, _current_state), do: _replace_symbolic_fns(already_normalized)
@predefined_transformers %{
halt: &__MODULE__.halt_transfo/1,
id: &__MODULE__.ident_transfo/1,
ignore: &__MODULE__.ignore_input/1,
}
@predefined_updaters %{
id: &__MODULE__.ident_updater/2
}
@spec _replace_symbolic_fns( incomplete_transition_t() ) :: transition_t()
defp _replace_symbolic_fns(transition)
defp _replace_symbolic_fns({trigger, f1, f2, state}) when is_atom(f1) do
_replace_symbolic_fns({trigger, Map.fetch!(@predefined_transformers, f1), f2, state})
end
defp _replace_symbolic_fns({trigger, f1, f2, state}) when is_atom(f2) do
_replace_symbolic_fns({trigger, f1, Map.fetch!(@predefined_updaters, f2), state})
end
defp _replace_symbolic_fns(really_ok_now), do: really_ok_now
@spec _run_normalized_transitions( state_t(), normalized_transitions_t(), list(), list(), any(), map() ) :: result_t()
defp _run_normalized_transitions(current_state, transitions, input, result, data, states)
defp _run_normalized_transitions(current_state, [{trigger,_,_,_}=tran|trans], [input|rest], result, data, states) do
# insp {:run_norm, current_state, tran, input, result, data}
if matches = _match_trigger(trigger, input) do
_execute_transition(tran, matches, input, data) |>
_loop(rest, result, states)
else
_run_transitions(current_state, trans, [input|rest], result, data, states)
end
end
@spec _run_transitions( state_t(), list(incomplete_transition_t()), list(), list(), any(), map() ) :: result_t()
defp _run_transitions(current_state, transitions, input, result, data, states)
defp _run_transitions(current_state, [], [input|_], _result, _data, _states) do
# It is preferable to not allow this, so that an explicit `true` trigger needs to be
# defined. One might later add an option `default_copy: true` to shorten the transition
# definitions if so is wished.
{:error, "No trigger matched the current input #{inspect input}", current_state}
end
defp _run_transitions(current_state, [tran|trans], input, result, data, states) do
# insp {:run_tr, current_state, tran, input, result, data}
_run_normalized_transitions(current_state, [_normalize_transition(tran, current_state)|trans], input, result, data, states)
end
@spec _loop( {state_t(), any(), any()}, list(), list(), map() ) :: result_t()
defp _loop(new_data_triple, rest, result, states)
defp _loop({new_state, :halt, updated}, _rest, result, _states) do
# Trigger halt with empty input
run(new_state, [], result, updated, %{})
end
defp _loop({new_state, {:halt, value}, updated}, _rest, result, _states) do
# Trigger halt with empty input
run(new_state, [], [value|result], updated, %{})
end
defp _loop({new_state, :ignore, updated}, rest, result, states) do
run(new_state, rest, result, updated, states)
end
defp _loop({new_state, {:push, transformed}, updated}, rest, result, states) do
run(new_state, rest, [transformed|result], updated, states)
end
defp _loop({new_state, transformed, updated}, rest, result, states) do
run(new_state, rest, [transformed|result], updated, states)
end
# TODO: Remove for release
# defp insp(data) do
# if System.get_env("DEBUG") do
# IO.inspect data
# end
# end
end