defmodule Flexflow.Event do @moduledoc """ Event """ alias Flexflow.Context alias Flexflow.Events.Blank alias Flexflow.Process alias Flexflow.State alias Flexflow.Util @type options :: Keyword.t() @type key :: Flexflow.state_type_or_module() | String.t() @type atom_result :: atom() @type event_result :: {:ok, atom_result()} | {:error, term()} @type t :: %__MODULE__{ module: module(), parent_module: module(), name: Flexflow.name(), from: Flexflow.state_key(), to: Flexflow.state_key(), __opts__: options, __op__: Flexflow.name(), results: MapSet.t(atom_result()), context: Context.t() } @enforce_keys [:name, :module, :from, :to, :results, :parent_module, :__op__] defstruct @enforce_keys ++ [__opts__: [], context: Context.new()] @doc "Module name" @callback name :: Flexflow.name() @doc "Invoked after compile, return :ok if valid" @callback init(t(), Process.t()) :: Process.result() @callback default_results :: [atom_result()] @callback validate(t(), Process.t()) :: :ok @callback graphviz_attribute :: keyword() @callback handle_input(term(), State.t(), Process.t()) :: event_result @callback is_event(term()) :: boolean() @callback handle_result(atom_result, Process.event_type(), term(), State.t(), Process.t()) :: Process.event_result() defmacro __using__(opts \\ []) do {inherit, opts} = Keyword.pop(opts, :inherit, Blank) quote do @behaviour unquote(__MODULE__) alias Flexflow.{Event, Process, State} require Logger unless Module.get_attribute(__MODULE__, :moduledoc) do @moduledoc """ See `#{unquote(__MODULE__)}` """ end defimpl(Flexflow.EventTracker, do: def(ping(_), do: :pong)) @__name__ Flexflow.Util.module_name(__MODULE__) def __opts__, do: unquote(opts) def __inherit__, do: unquote(if __MODULE__ == inherit, do: nil, else: inherit) @impl true def name, do: @__name__ if __MODULE__ != unquote(inherit) do unless Util.local_behaviour(unquote(inherit)) == unquote(__MODULE__) do raise ArgumentError, "Invalid inherit module: #{inspect(unquote(inherit))}" end defdelegate graphviz_attribute, to: Blank defdelegate is_event(t), to: Blank defdelegate default_results, to: unquote(inherit) defdelegate init(a, p), to: unquote(inherit) defdelegate validate(a, p), to: unquote(inherit) defdelegate handle_input(term, s, p), to: unquote(inherit) defdelegate handle_result(result, t, term, s, p), to: unquote(inherit) end defoverridable unquote(__MODULE__) Module.delete_attribute(__MODULE__, :__name__) end end @spec key(t()) :: {Flexflow.state_key(), Flexflow.state_key()} def key(%{from: from, to: to}), do: {from, to} @spec new({key(), {key(), key()}, options}, [State.t()], Process.process_tuple()) :: t() def new({_o, {from, _to}, _opts}, _states, _process_tuple) when is_binary(from), do: raise(ArgumentError, "Name `#{from}` should be an atom") def new({_o, {_from, to}, _opts}, _states, _process_tuple) when is_binary(to), do: raise(ArgumentError, "Name `#{to}` should be an atom") def new({o, {_from, _to}, _opts}, _states, _process_tuple) when is_binary(o), do: raise(ArgumentError, "Name `#{o}` should be an atom") def new({o, {from, to}, opts}, states, process_tuple) do from = Util.normalize_module(from, states) to = Util.normalize_module(to, states) new_1({o, {from, to}, opts}, states, process_tuple) end defp new_1({o, {from, to}, opts}, states, process_tuple) when is_atom(o) do new_1( {Util.normalize_module({o, from, to}, states), {from, to}, opts}, states, process_tuple ) end defp new_1( {{o, name}, {{_, from_name} = from, {_, to_name} = to}, opts}, states, {_, process_name} = process_tuple ) do unless Util.local_behaviour(o) == __MODULE__ do raise ArgumentError, "`#{inspect(o)}` should implement #{__MODULE__}" end states = Map.new(states, &{&1.name, &1}) from_state = states[from_name] || states[String.to_atom("#{process_name}_s_#{from_name}")] from_state || raise(ArgumentError, "`#{inspect(from)}` is not defined") to_state = states[to_name] || states[String.to_atom("#{process_name}_s_#{to_name}")] to_state || raise(ArgumentError, "`#{inspect(to)}` is not defined") opts = opts ++ o.__opts__ {results, opts} = Keyword.pop(opts, :results) {ast, opts} = Keyword.pop(opts, :do) {new_module, name} = new_module(ast, o, name, process_tuple) results = results || new_module.default_results() %__MODULE__{ module: new_module, name: name, from: from_state.name, to: to_state.name, __opts__: opts, results: MapSet.new(results), parent_module: o, __op__: o.name } end defp new_module(nil, parent_module, name, _), do: {parent_module, name} defp new_module(ast, parent_module, name, {process_module, process_name}) do module_name = Module.concat([process_module, parent_module, Macro.camelize(to_string(name))]) name = String.to_atom("#{process_name}_e_#{name}") ast = quote generated: true do use unquote(__MODULE__), inherit: unquote(parent_module) alias unquote(parent_module) unquote(ast) @impl true def name, do: unquote(name) end {:module, ^module_name, _byte_code, _} = Module.create(module_name, ast, Macro.Env.location(__ENV__)) {module_name, name} end @spec validate([t()]) :: [t()] def validate(events) do if Enum.empty?(events), do: raise(ArgumentError, "Event is empty") for %__MODULE__{module: module, name: name, from: from, to: to, results: results, __op__: op} <- events, reduce: {[], [], %{}} do {ary1, ary2, map} -> o1 = {module, name} if o1 in ary1, do: raise(ArgumentError, "Event `#{inspect(o1)}` is defined twice") o2 = {from, to} if o2 in ary2, do: raise(ArgumentError, "Event `#{inspect(o2)}` is defined twice") map = case map[{from, op}] do nil -> Map.put(map, {from, op}, results) mapset -> if MapSet.disjoint?(mapset, results) do Map.put(map, {from, op}, MapSet.union(mapset, results)) else raise(ArgumentError, "Event #{name} has duplicate results: #{inspect(results)}") end end {[o1 | ary1], [o2 | ary2], map} end events end @spec init(Process.t()) :: Process.result() def init(%{events: events} = p) do Enum.reduce_while(events, p, fn {key, event}, p -> case event.module.init(event, p) do {:ok, event} -> {:cont, put_in(p, [:events, key], event)} {:error, reason} -> {:halt, {:error, reason}} end end) |> case do {:error, reason} -> {:error, reason} %Process{} = p -> {:ok, p} end end end defmodule Flexflow.Events.Blank do @moduledoc false use Flexflow.Event @impl true def name, do: :blank @impl true def default_results, do: [:ignore] @impl true def init(e, _), do: {:ok, e} @impl true def graphviz_attribute, do: [] @impl true def validate(_, _), do: :ok @impl true def handle_input(_, _, _), do: {:ok, :ignore} @impl true def is_event(_), do: true @impl true def handle_result(:ignore, _, _, _, _), do: :ignore end