Current section
Files
Jump to
Current section
Files
lib/callbackex.ex
defmodule Callbackex do
@moduledoc """
Define a `Callbackex`
A callbackex is used to define, organize and invoke callbacks
# Example
```
defmodule UserProcessor do
# Use Callbackex
use Callbackex, :before_create, :after_create
# Define callbacks
callbacks do
before_create :check_ip
before_create User.ValidateName, limit: 10
after_create Indexer, index: :user
after_create AuditLog, operation: :create
end
# Use callbacks
def create(params) do
with {:ok, params} <- invoke_callback(:before_create, params),
{:ok, user} <- do_create_user(params),
{:ok, user} <- invoke_callback(:after_create, user) do
{:ok, user}
end
end
end
```
# Reflection
Any callbackex module will generate the __callbackex__ function that can be used for runtime introspection of the callbacks
* `__callbackex__(:callbacks)` - Returns the callback names defined
* `__callbackex__(:callback, callback)` - Returns the callback call corresponding to given callback
"""
@callbacks :callbackex_callbacks
@callback_registry :callbackex_registry
alias Callbackex.Callbacks
defmacro __using__(opts) do
module = __CALLER__.module
Module.register_attribute module, @callbacks, accumulate: false, persist: false
Module.register_attribute module, @callback_registry, accumulate: false, persist: false
Module.put_attribute module, @callbacks, opts
Module.put_attribute module, @callback_registry, %{}
quote do
@before_compile unquote(__MODULE__)
import unquote(__MODULE__)
def invoke_callback(callback, value) do
context = Callbackex.do_invoke_callback(__MODULE__, callback, value)
context.result
end
end
end
@spec callbacks(Macro.t) :: Macro.t
defmacro callbacks([do: block]) do
expand(__CALLER__.module, block)
end
@spec do_invoke_callback(module, atom, any) :: Callbackex.Context.t
def do_invoke_callback(module, callback, value) do
call = module.__callbackex__(:callback, callback)
call.(module, value)
end
defp expand(module, block) do
callbacks = Module.get_attribute(module, @callbacks) |> Enum.map(&({&1, []})) |> Enum.into(%{})
{_, callbacks} =
Macro.prewalk(block, callbacks, fn
{_, _, _} = ast, callbacks ->
try_register_callback(ast, callbacks)
ast, callbacks ->
{ast, callbacks}
end)
init_value = quote do
Module.get_attribute(__MODULE__, unquote(@callback_registry))
end
callback_definitions = Enum.reduce(callbacks, init_value, "e_callback_definitions/2)
quote do
Module.put_attribute(__MODULE__, unquote(@callback_registry), unquote(callback_definitions))
end
end
defp try_register_callback({method, _, args} = ast, callbacks) do
# try to register callbacks in *reversed order*
if Map.has_key?(callbacks, method) do
callbacks = Map.update!(callbacks, method, fn
callback_list -> [check_args(args) | callback_list]
end)
{nil, callbacks}
else
{ast, callbacks}
end
end
defp check_args([callback, opts]) when (is_atom(callback) or is_tuple(callback)) do
{callback, opts}
end
defp check_args([callback]) when (is_atom(callback) or is_tuple(callback)) do
{callback, []}
end
defp check_args(args) do
declaration = Macro.to_string(args)
raise ArgumentError, "Illegal callback declareration #{declaration}"
end
defp quote_callback_definitions({callback_name, definitions}, acc) do
quoted_definitions = Enum.map(definitions, "e_definition/1)
quote do
Map.put(unquote(acc), unquote(callback_name), [unquote_splicing(quoted_definitions)])
end
end
defp quote_definition({callback, opts}) do
quote do
{unquote(callback), unquote(opts)}
end
end
defmacro __before_compile__(env) do
callbacks = Module.get_attribute(env.module, @callbacks)
callback_registry = Module.get_attribute(env.module, @callback_registry)
[
quote do
def __callbackex__(:callbacks), do: unquote(callbacks)
end,
define_callback_calls(callback_registry),
quote do
def __callbackex__(:callback, callback), do: raise ArgumentError, "Undefined callback #{inspect callback}"
end
]
end
defp define_callback_calls(callback_registry) do
Enum.map(callback_registry, fn
{callback, configs} ->
# The order or configs is *reversed*
call = Callbacks.compile(configs)
quote do
def __callbackex__(:callback, unquote(callback)), do: unquote(call)
end
end)
end
end