Current section

Files

Jump to
jsv lib jsv debanger.ex
Raw

lib/jsv/debanger.ex

defmodule JSV.Debanger do
@moduledoc false
defmacro __using__(opts) do
quote do
import unquote(__MODULE__)
@before_compile unquote(__MODULE__)
@__debang_nowrap_records unquote(List.wrap(opts[:records]))
Module.register_attribute(__MODULE__, :__debang_bang_funs, accumulate: true)
end
end
defmacro debang(call) do
{:def, meta, [{fun, _, args}]} = call
quote do
@__debang_bang_funs {unquote(fun), unquote(meta), unquote(Macro.escape(args)),
Module.get_attribute(__MODULE__, :doc_group)}
unquote(call)
end
end
defmacro __before_compile__(env) do
bang_funs = Module.get_attribute(env.module, :__debang_bang_funs)
specs = Module.get_attribute(env.module, :spec)
quoted_unbanged =
Enum.map(bang_funs, fn {bang_fun, bang_meta, args, doc_group} ->
{:spec, {:"::", _, [{^bang_fun, _, arg_types}, return_type]}, _} = find_spec!(specs, bang_fun)
args_no_defaults = args_no_defaults(args)
tuple_return_type = return_type_to_tuple_type(return_type)
nobang_fun = debang_atom(bang_fun)
quoted =
quote do
@doc """
Same as `#{unquote(bang_fun)}/#{unquote(length(args))}` but rescues
errors and returns a result tuple.
"""
@spec unquote(nobang_fun)(unquote_splicing(arg_types)) :: unquote(tuple_return_type)
if doc_group = unquote(doc_group) do
@doc group: doc_group
end
def unquote(nobang_fun)(unquote_splicing(args)) do
__debang_wrap__(unquote(bang_fun)(unquote_splicing(args_no_defaults)))
rescue
e -> {:error, e}
end
end
Macro.postwalk(quoted, fn
node -> Macro.update_meta(node, &Keyword.merge(bang_meta, &1))
end)
end)
quote generated: true do
unquote(quoted_unbanged)
if @__debang_nowrap_records != [] do
defp __debang_wrap__(value) when is_tuple(value) and elem(value, 0) in @__debang_nowrap_records do
{:ok, value}
end
end
defp __debang_wrap__(value) do
unquote(__MODULE__).__wrap__(value)
end
end
end
defp find_spec!(specs, bang_fun) do
spec =
Enum.find_value(specs, fn
{:spec, {:"::", _, [{^bang_fun, _, _}, _]}, _} = spec -> spec
_ -> nil
end)
if spec == nil do
raise "could not debang fun #{inspect(bang_fun)}, no spec found"
end
spec
end
defp return_type_to_tuple_type(return_type) do
case return_type do
{:{}, meta, tuple_vals} ->
{:|, meta,
[
{:{}, meta, [:ok | tuple_vals]},
{:error,
quote do
Exception.t()
end}
]}
# 2-tuples are not quoted
{t1, t2} ->
{:|, [],
[
{:{}, [], [:ok, t1, t2]},
{:error, {{:., [], [:"Elixir.Exception", :t]}, [], []}}
]}
raw_type ->
{:|, [],
[
{:ok, raw_type},
{:error, {{:., [], [:"Elixir.Exception", :t]}, [], []}}
]}
end
end
defp debang_atom(atom) do
atom
|> Atom.to_string()
|> tap(&(true = String.ends_with?(&1, "!")))
|> String.trim_trailing("!")
|> String.to_atom()
end
defp args_no_defaults(args) do
Macro.prewalk(args, fn
{:\\, _, [var, _default]} -> var
ast -> ast
end)
end
@spec __wrap__(term) :: tuple
def __wrap__(value) do
case value do
{a, b} -> {:ok, a, b}
{a, b, c} -> {:ok, a, b, c}
{a, b, c, d} -> {:ok, a, b, c, d}
{a, b, c, d, e} -> {:ok, a, b, c, d, e}
v when not is_tuple(v) -> {:ok, v}
end
end
end