# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved.
use Croma
defmodule Antikythera.Controller.Plug do
@moduledoc """
Macro definition for plug DSL.
"Plug" is a specification of composable modules for web apps.
For example, you can define your authentication logic as a plug and apply it to all your controller actions.
## Usage
To use plug you must first add `use Antikythera.Controller` in your controller module.
Then you can invoke `plug/3` macro as follows.
plug Antikythera.Plug.BasicAuthentication, :check_with_config, []
The arguments are
1. module
2. function name
3. options to be passed to plug function
4. (optional) options for enabling plug
In this case `Antikythera.Plug.BasicAuthentication.check_with_config/2` is invoked just before execution of actions
defined in this controller module.
If you want to skip running plug for some actions in a controller module, you can use `:except` or `:only` option.
plug YourGear.SomePlug1, :do_something, [], [except: [:action_x, :action_y]]
plug YourGear.SomePlug2, :do_something, [], [only: [:action_x]]
"""
defmacro __using__(_opts) do
quote do
import Antikythera.Controller.Plug
Module.register_attribute(__MODULE__, :plugs, accumulate: true)
@before_compile Antikythera.Controller.Plug
end
end
defmacro __before_compile__(_env) do
quote do
@plugs_reversed Enum.reverse(@plugs)
def __action__(conn, action) do
# Be careful when you edit the body of this function: changes won't be applied until all gears are re-compiled.
Antikythera.Controller.Plug._run_action(conn, action, __MODULE__, @plugs_reversed)
end
end
end
defmacro plug(mod, func, plug_arg, opts \\ []) when is_atom(func) do
plug_impl(mod, func, plug_arg, opts)
end
defp plug_impl(module, func, plug_arg, opts) do
except = Keyword.get(opts, :except, [])
only = Keyword.get(opts, :only, [])
if !Enum.all?(except, &is_atom/1), do: raise(":except must be a list of action names (atoms)")
if !Enum.all?(only, &is_atom/1), do: raise(":only must be a list of action names (atoms)")
if !Enum.empty?(except) and !Enum.empty?(only),
do: raise("either :except or :only must be empty")
quote bind_quoted: [
module: module,
func: func,
plug_arg: plug_arg,
except: except,
only: only
] do
@plugs {module, func, plug_arg, except, only}
end
end
@doc false
def _run_action(conn, action, controller, plugs_reversed) do
AntikytheraCore.GearLog.ContextHelper.set(conn)
plugs =
Enum.filter(plugs_reversed, fn
{_, _, _, [], []} -> true
{_, _, _, except_actions, []} -> action not in except_actions
{_, _, _, [], only_actions} -> action in only_actions
end)
run_action_with_plugs(conn, controller, action, plugs)
end
defp run_action_with_plugs(conn, controller, action, []) do
apply(controller, action, [conn])
end
defp run_action_with_plugs(conn, controller, action, [{mod, fun, arg, _except, _only} | plugs]) do
%Antikythera.Conn{status: status} = conn2 = apply(mod, fun, [conn, arg])
case status do
nil -> run_action_with_plugs(conn2, controller, action, plugs)
_ -> conn2
end
end
end