Packages

Minimalist Function Decorators

Current section

Files

Jump to
deco lib deco.ex
Raw

lib/deco.ex

defmodule Deco do
@moduledoc File.read!(Path.expand("../README.md", __DIR__))
@doc false
defmacro __using__(_) do
quote do
import Deco.Core, only: [deco: 2]
end
end
@type updater :: (Macro.t() -> Macro.t())
@doc """
Pipes the result of the decorated function into `pipe`.
# Example
deco {Deco.pipe_result(to_string)} in
def foo(x), do: x
foo(:hello)
=> "hello"
"""
@spec pipe_result(defun :: Macro.t(), pipe :: Macro.t()) :: Macro.t()
defmacro pipe_result(defun, pipe) do
quote bind_quoted: [pipe: Macro.escape(pipe), defun: defun] do
defun |> Deco.update_do(fn body -> {:|>, [], [body, pipe]} end)
end
end
@doc """
Lets you update the decorated function body
Note that `updater` function takes the quoted
body and should also return another quoted
expression.
deco {Deco.update_do(fn _ -> :yes end)}
def foo, do: :ok
foo()
=> :yes
Using this decorator you can rescue exceptions
and turn them into erlang tagged tuples:
defmodule MyDecos do
def ok_or_error(defun) do
defun |> Deco.update_do(fn body ->
quote do
try do
{:ok, unquote(body)}
rescue
error -> {:error, error}
end
end
end)
end
end
deco {MyDecos.ok_or_error} in
def foo() do
raise "Oops"
end
foo()
=> {:error, %RuntimeError{message: "Oops"}}
"""
@spec update_do(defun :: Macro.t(), updater :: updater) :: Macro.t()
def update_do(defun, updater)
def update_do(expr = {_def, _, _}, fun) do
update_def_key(expr, do: fun)
end
@doc "Alias for update_do"
@spec update_body(defun :: Macro.t(), updater :: updater) :: Macro.t()
def update_body(defun, updater) do
update_do(defun, updater)
end
@doc """
Lets you update the function definition head
"""
@spec update_head(defun :: Macro.t(), updater :: updater) :: Macro.t()
def update_head(defun, updater)
def update_head({defn, m, [{:when, _, [head, guard]}, opts]}, fun) do
update_head(defn, head, m, guard, opts, fun)
end
def update_head({defn, m, [head, opts]}, fun) do
update_head(defn, head, m, _guard = nil, opts, fun)
end
defp update_head(defn, head, m, guard, opts, fun) do
head = fun.(head)
case guard do
nil -> {defn, m, [head, opts]}
guard -> {defn, m, [{:when, m, [head, guard]}, opts]}
end
end
@doc """
Get the head of the function definition. `name(args)`
"""
@spec get_head(defun :: Macro.t()) :: Macro.t()
def get_head(defun)
def get_head({_def, _, [{:when, _, [head, _]}, _]}), do: head
def get_head({_def, _, [head, _]}), do: head
@spec get_name(defun :: Macro.t()) :: atom
def get_name(defun) do
{name, _, _args} = get_head(defun)
name
end
@spec get_args(defun :: Macro.t()) :: list(Macro.t())
def get_args(defun) do
{_, _, args} = get_head(defun)
args
end
@spec update_args(defun :: Macro.t(), updater) :: Macro.t()
def update_args(defun, updater) do
update_head(defun, fn {name, m, args} ->
{name, m, updater.(args)}
end)
end
@doc """
Creates a list of fresh variables, one per each formal argument in head.
"""
def fresh_args(defun, context \\ __MODULE__) do
defun |> get_args |> Enum.count |> Macro.generate_arguments(context)
end
@doc """
Make the function definition private, prefixing its name with `prefix`
"""
def privatize(defun, prefix) do
defun = update_head(defun, fn {name, c, a} -> {:"#{prefix}#{name}", c, a} end)
case defun do
{:def, c, d} -> {:defp, c, d}
{:defmacro, c, d} -> {:defmacrop, c, d}
x -> x
end
end
@doc """
Generates and binds a new variable to each formal parameter
Returns a tuple of the updated defun and a list of the bound argument variables.
"""
@spec intro_args(defun :: Macro.t(), context :: atom) :: {Macro.t(), list(Macro.t())}
def intro_args(defun, context \\ __MODULE__) do
vars = fresh_args(defun, context)
args = get_args(defun)
args = Enum.zip([vars, args]) |> Enum.map(fn {v, a} ->
quote do: unquote(v) = unquote(a)
end)
defun = update_args(defun, fn _ -> args end)
{defun, vars}
end
@doc """
Lets you update or remove the function definition guard.
The `updater` function will receive the AST of the current guard
or nil if none was defined.
If nil is returned by `updater`, the guard is removed from
the decorated function.
"""
@spec update_guard(defun :: Macro.t(), updater :: updater) :: Macro.t()
def update_guard(defun, updater)
def update_guard({defn, m, [{:when, _, [head, guard]}, opts]}, fun) do
update_guard(defn, guard, m, head, opts, fun)
end
def update_guard({defn, m, [head, opts]}, fun) do
update_guard(defn, nil, m, head, opts, fun)
end
defp update_guard(defn, guard, m, head, opts, fun) do
case fun.(guard) do
nil -> {defn, m, [head, opts]}
guard -> {defn, m, [{:when, m, [head, guard]}, opts]}
end
end
defp update_def_key({defn, meta, [head, opts]}, [{key, fun}]) do
opts = update_in(opts[key], fun)
{defn, meta, [head, opts]}
end
@doc ~S"""
Wraps the decorated function invocation with another call.
The wrapper function will take as arguments: a function reference to the
decorated function, all the arguments given to the decorated function
invocation and all the arguments given in the decorator declaration.
defp bar_wrapper(decorated, name, say) do
"#{say} #{decorated.(name)}"
end
deco {Deco.around( bar_wrapper("hello") )} in
def bar(name) do
name |> String.capitalize
end
bar("world")
=> "hello World"
This function works by making the original function definition private
and calling your wrapper with a reference to the private function, this
way you can alter or prevent the original function invocation like an
around advice.
"""
defmacro around(defun, wrapper) do
quote bind_quoted: [wrapper: Macro.escape(wrapper), defun: defun] do
private = Deco.privatize(defun, "__wrap#{:erlang.unique_integer([:positive])}_")
priv_name = Deco.get_name(private)
args = Deco.fresh_args(defun)
ref = {:&, [], [{:/, [], [{priv_name, [], nil}, length(args)]}]}
call_wrapper = Deco.Core.add_args(:prepend, wrapper, [ref] ++ args)
defun = defun
|> Deco.update_args(fn _ -> args end)
|> Deco.update_guard(fn _ -> nil end)
|> Deco.update_body(fn _ -> call_wrapper end)
{:__block__, [], [defun, private]}
end
end
end