defmodule SpawnSdk.Defact do @moduledoc """ Define actions like a Elixir functions ### Internal :defact_exports metadata saved as [ {action_name, %{timer: 10_000}}, {action_name2, %{timer: nil}} ] """ @default_actions ~w(get Get get_state getState GetState) defmacro __using__(_args) do quote do import SpawnSdk.Defact Module.register_attribute(__MODULE__, :defact_exports, accumulate: true) @set_timer nil end end defmacro init(block_fn) do action_name = "init" define_action(action_name, block_fn) end defmacro action(action_name, opts, block_fn) do define_action(action_name, block_fn, opts) end defmacro action(action_name, block_fn) do define_action(action_name, block_fn) end defp define_action(action_name, block_fn, opts \\ []) do if action_name in @default_actions do raise SpawnSdk.Actor.MalformedActor, "Action name #{action_name} is reserved" end quote do Module.put_attribute( __MODULE__, :defact_exports, Macro.escape({unquote(action_name), %{timer: Keyword.get(unquote(opts), :timer)}}) ) def handle_action({unquote(action_name), payload}, context) do case {:erlang.fun_info(unquote(block_fn), :arity), unquote(action_name)} do {{:arity, 1}, _name} -> unquote(block_fn).(context) {{:arity, 2}, action} when action not in ~w(init Init Setup setup) -> unquote(block_fn).(context, payload) {{:arity, arity}, _} -> raise SpawnSdk.Actor.MalformedActor, "Invalid callback arity #{arity} needs to be in (1, 2) for action=#{unquote(action_name)}" end |> case do %SpawnSdk.Value{} = value -> value {:reply, %SpawnSdk.Value{} = value} -> value _ -> raise SpawnSdk.Actor.MalformedActor, "Return value for action=#{unquote(action_name)} must be a %Value{} struct" end end end end defmacro defact(call, do: block) do define_defact(:def, call, block, __CALLER__) end defp define_defact(kind, call, block, env) do {name, args} = decompose_call!(kind, call, env) {payload, context} = case args do [payload, context] -> {payload, context} [context] -> {nil, context} end if is_nil(payload) do quote do Module.put_attribute( __MODULE__, :defact_exports, Macro.escape({unquote(name), %{timer: @set_timer}}) ) def handle_action({unquote(name), _}, unquote(context)) do unquote(block) end end else quote do Module.put_attribute( __MODULE__, :defact_exports, Macro.escape({unquote(name), %{timer: @set_timer}}) ) def handle_action({unquote(name), unquote(payload)}, unquote(context)) do unquote(block) end end end end defp decompose_call!(kind, {:when, _, [call, _guards]}, env), do: decompose_call!(kind, call, env) defp decompose_call!(_kind, {{:unquote, _, [name]}, _, args}, _env) do {parse_action_name(name), args} end defp decompose_call!(kind, call, env) do case Macro.decompose_call(call) do {name, args} -> {parse_action_name(name), args} :error -> compile_error!( env, "first argument of #{kind}n must be a call, got: #{Macro.to_string(call)}" ) end end defp parse_action_name(action) when is_atom(action), do: Atom.to_string(action) defp parse_action_name(action) when is_binary(action), do: action defp compile_error!(env, description) do raise CompileError, line: env.line, file: env.file, description: description end end