defmodule Fsm do defmacro __using__(opts) do quote do import Fsm defstruct state: unquote(opts[:initial_state]), data: unquote(opts[:initial_data]) @declaring_state nil @declared_events MapSet.new() def new(params \\ []), do: struct!(__MODULE__, params) def state(%__MODULE__{state: state}), do: state def data(%__MODULE__{data: data}), do: data # We need to suppress dialyzer warning, since in the second clause might # never match. This can happen if every single event handler uses `Fsm` # functions to explicitly set the next state. @dialyzer {:no_match, change_state: 2} defp change_state(%__MODULE__{} = fsm, {:action_responses, responses}), do: parse_action_responses(fsm, responses) defp change_state(%__MODULE__{} = fsm, _), do: fsm defp parse_action_responses(fsm, responses) do Enum.reduce(responses, fsm, fn response, fsm -> handle_action_response(fsm, response) end) end defp handle_action_response(fsm, {:next_state, next_state}) do %__MODULE__{fsm | state: next_state} end defp handle_action_response(fsm, {:new_data, new_data}) do %__MODULE__{fsm | data: new_data} end defp handle_action_response(fsm, {:respond, response}) do {response, fsm} end end end def next_state(state), do: {:action_responses, [next_state: state]} def next_state(state, data), do: {:action_responses, [next_state: state, new_data: data]} def respond(response), do: {:action_responses, [respond: response]} def respond(response, state), do: {:action_responses, [next_state: state, respond: response]} def respond(response, state, data), do: {:action_responses, [next_state: state, new_data: data, respond: response]} defmacro defstate(state, state_def) do quote do state_name = case unquote(Macro.escape(state, unquote: true)) do name when is_atom(name) -> name {name, _, _} -> name end @declaring_state state_name unquote(state_def) @declaring_state nil end end defmacro defevent(event) do decl_event(event, false) end defmacro defeventp(event) do decl_event(event, true) end defp decl_event(event, private) do quote do {event_name, arity} = case unquote(Macro.escape(event, unquote: nil)) do event_name when is_atom(event_name) -> {event_name, 0} {:/, _, [{event_name, _, _}, arity]} -> {event_name, arity} {event_name, _, _} -> {event_name, 0} end args = case arity do 0 -> [] n -> Enum.to_list(1..n) end private = unquote(private) unquote(define_interface()) end end defmacro defevent(event, opts) do do_defevent(event, opts, opts[:do]) end defmacro defevent(event, opts, do: event_def) do do_defevent(event, opts, event_def) end defmacro defeventp(event, opts) do do_defevent(event, [{:private, true} | opts], opts[:do]) end defmacro defeventp(event, opts, do: event_def) do do_defevent(event, [{:private, true} | opts], event_def) end defp do_defevent(event_decl, opts, event_def) do quote do unquote(extract_args(event_decl, opts, event_def)) unquote(define_interface()) unquote(implement_transition()) end end defp extract_args(event_decl, opts, event_def) do quote do {event_name, args} = case unquote(Macro.escape(event_decl, unquote: true)) do :_ -> {:_, []} name when is_atom(name) -> {name, []} {name, _, args} -> {name, args || []} end private = unquote(opts[:private]) state_arg = unquote(Macro.escape(opts[:state] || quote(do: _), unquote: true)) data_arg = unquote(Macro.escape(opts[:data] || quote(do: _), unquote: true)) event_arg = unquote(Macro.escape(opts[:event] || quote(do: _), unquote: true)) args_arg = unquote(Macro.escape(opts[:args] || quote(do: _), unquote: true)) event_def = unquote(Macro.escape(event_def, unquote: true)) guard = unquote(Macro.escape(opts[:when])) end end defp define_interface do quote bind_quoted: [] do unless event_name == :_ or MapSet.member?(@declared_events, {event_name, length(args)}) do interface_args = Enum.reduce(args, {0, []}, fn _, {index, args} -> { index + 1, [{:"arg#{index}", [], nil} | args] } end) |> elem(1) |> Enum.reverse() body = quote do transition(fsm, unquote(event_name), [unquote_splicing(interface_args)]) end interface_args = [quote(do: fsm) | interface_args] if private do defp unquote(event_name)(unquote_splicing(interface_args)), do: unquote(body) else def unquote(event_name)(unquote_splicing(interface_args)), do: unquote(body) end @declared_events MapSet.put(@declared_events, {event_name, length(args)}) end end end defp implement_transition do quote bind_quoted: [] do transition_args = [ if @declaring_state do quote do %__MODULE__{ state: unquote(@declaring_state) = unquote(state_arg), data: unquote(data_arg) } = fsm end else quote do %__MODULE__{state: unquote(state_arg), data: unquote(data_arg)} = fsm end end, quote do unquote(if event_name == :_, do: quote(do: _), else: event_name) = unquote(event_arg) end, quote do unquote(if event_name == :_, do: quote(do: _), else: args) = unquote(args_arg) end ] body = quote(do: change_state(fsm, unquote(event_def))) if guard do def transition(unquote_splicing(transition_args)) when unquote(guard), do: unquote(body) else def transition(unquote_splicing(transition_args)), do: unquote(body) end end end end