Current section

Files

Jump to
combo lib combo controller pipeline.ex
Raw

lib/combo/controller/pipeline.ex

defmodule Combo.Controller.Pipeline do
@moduledoc false
@doc false
defmacro __using__(_) do
quote do
@behaviour Plug
import Combo.Controller.Pipeline
Module.register_attribute(__MODULE__, :plugs, accumulate: true)
@before_compile Combo.Controller.Pipeline
@combo_fallback :unregistered
@doc false
def init(opts), do: opts
@doc false
def call(conn, action) when is_atom(action) do
conn
|> Plug.Conn.merge_private(
combo_controller: __MODULE__,
combo_action: action
)
|> combo_controller_pipeline(action)
end
@doc false
def action(%Plug.Conn{private: %{combo_action: action}} = conn, _options) do
apply(__MODULE__, action, [conn, conn.params])
end
defoverridable init: 1, call: 2, action: 2
end
end
@doc false
def __action_fallback__(plug, caller) do
plug = Macro.expand(plug, %{caller | function: {:init, 1}})
quote bind_quoted: [plug: plug] do
@combo_fallback Combo.Controller.Pipeline.validate_fallback(
plug,
__MODULE__,
Module.get_attribute(__MODULE__, :combo_fallback)
)
end
end
@doc false
def validate_fallback(plug, module, fallback) do
cond do
fallback == nil ->
raise """
action_fallback can only be called when using Combo.Controller.
Add `use Combo.Controller` to #{inspect(module)}
"""
fallback != :unregistered ->
raise "action_fallback can only be called a single time per controller."
not is_atom(plug) ->
raise ArgumentError,
"expected action_fallback to be a module or function plug, got #{inspect(plug)}"
fallback == :unregistered ->
case Atom.to_charlist(plug) do
~c"Elixir." ++ _ -> {:module, plug}
_ -> {:function, plug}
end
end
end
@doc false
defmacro __before_compile__(env) do
action = {:action, [], true}
plugs = [action | Module.get_attribute(env.module, :plugs)]
{conn, body} =
Plug.Builder.compile(env, plugs,
log_on_halt: :debug,
init_mode: Combo.plug_init_mode()
)
fallback_ast =
env.module
|> Module.get_attribute(:combo_fallback)
|> build_fallback()
quote do
defoverridable action: 2
def action(var!(conn_before), opts) do
try do
var!(conn_after) = super(var!(conn_before), opts)
unquote(fallback_ast)
catch
:error, reason ->
Combo.Controller.Pipeline.__catch__(
var!(conn_before),
reason,
__MODULE__,
var!(conn_before).private.combo_action,
__STACKTRACE__
)
end
end
defp combo_controller_pipeline(unquote(conn), var!(action)) do
var!(conn) = unquote(conn)
var!(controller) = __MODULE__
_ = var!(conn)
_ = var!(controller)
_ = var!(action)
unquote(body)
end
end
end
defp build_fallback(:unregistered) do
quote do: var!(conn_after)
end
defp build_fallback({:module, plug}) do
quote bind_quoted: binding() do
case var!(conn_after) do
%Plug.Conn{} = conn_after -> conn_after
val -> plug.call(var!(conn_before), plug.init(val))
end
end
end
defp build_fallback({:function, plug}) do
quote do
case var!(conn_after) do
%Plug.Conn{} = conn_after -> conn_after
val -> unquote(plug)(var!(conn_before), val)
end
end
end
@doc false
@dialyzer {:no_return, __catch__: 5}
def __catch__(
%Plug.Conn{},
:function_clause,
controller,
action,
[{controller, action, [%Plug.Conn{} | _] = action_args, _loc} | _] = stack
) do
args = [module: controller, function: action, arity: length(action_args), args: action_args]
reraise Combo.ActionClauseError, args, stack
end
def __catch__(%Plug.Conn{} = conn, reason, _controller, _action, stack) do
Plug.Conn.WrapperError.reraise(conn, :error, reason, stack)
end
@doc """
Stores a plug to be executed as part of the plug pipeline.
"""
defmacro plug(plug)
defmacro plug({:when, _, [plug, guards]}), do: plug(plug, [], guards, __CALLER__)
defmacro plug(plug), do: plug(plug, [], true, __CALLER__)
@doc """
Stores a plug with the given options to be executed as part of
the plug pipeline.
"""
defmacro plug(plug, opts)
defmacro plug(plug, {:when, _, [opts, guards]}), do: plug(plug, opts, guards, __CALLER__)
defmacro plug(plug, opts), do: plug(plug, opts, true, __CALLER__)
defp plug(plug, opts, guards, caller) do
runtime? = Combo.plug_init_mode() == :runtime
plug =
if runtime? do
expand_alias(plug, caller)
else
plug
end
opts =
if runtime? and Macro.quoted_literal?(opts) do
Macro.prewalk(opts, &expand_alias(&1, caller))
else
opts
end
quote do
@plugs {unquote(plug), unquote(opts), unquote(escape_guards(guards))}
end
end
defp expand_alias({:__aliases__, _, _} = alias, env),
do: Macro.expand(alias, %{env | function: {:init, 1}})
defp expand_alias(other, _env), do: other
defp escape_guards({pre_expanded, _, [_ | _]} = node)
when pre_expanded in [:@, :__aliases__],
do: node
defp escape_guards({left, meta, right}),
do: {:{}, [], [escape_guards(left), meta, escape_guards(right)]}
defp escape_guards({left, right}),
do: {escape_guards(left), escape_guards(right)}
defp escape_guards([_ | _] = list),
do: Enum.map(list, &escape_guards/1)
defp escape_guards(node),
do: node
end