zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

builder.ex (12051B)


      1 defmodule Plug.Builder do
      2   @moduledoc """
      3   Conveniences for building plugs.
      4 
      5   You can use this module to build a plug pipeline:
      6 
      7       defmodule MyApp do
      8         use Plug.Builder
      9 
     10         plug Plug.Logger
     11         plug :hello, upper: true
     12 
     13         # A function from another module can be plugged too, provided it's
     14         # imported into the current module first.
     15         import AnotherModule, only: [interesting_plug: 2]
     16         plug :interesting_plug
     17 
     18         def hello(conn, opts) do
     19           body = if opts[:upper], do: "WORLD", else: "world"
     20           send_resp(conn, 200, body)
     21         end
     22       end
     23 
     24   Multiple plugs can be defined with the `plug/2` macro, forming a pipeline.
     25   The plugs in the pipeline will be executed in the order they've been added
     26   through the `plug/2` macro. In the example above, `Plug.Logger` will be
     27   called first and then the `:hello` function plug will be called on the
     28   resulting connection.
     29 
     30   `Plug.Builder` also imports the `Plug.Conn` module, making functions like
     31   `send_resp/3` available.
     32 
     33   ## Options
     34 
     35   When used, the following options are accepted by `Plug.Builder`:
     36 
     37     * `:init_mode` - the environment to initialize the plug's options, one of
     38       `:compile` or `:runtime`. Defaults `:compile`.
     39 
     40     * `:log_on_halt` - accepts the level to log whenever the request is halted
     41 
     42     * `:copy_opts_to_assign` - an `atom` representing an assign. When supplied,
     43       it will copy the options given to the Plug initialization to the given
     44       connection assign
     45 
     46   ## Plug behaviour
     47 
     48   Internally, `Plug.Builder` implements the `Plug` behaviour, which means both
     49   the `init/1` and `call/2` functions are defined.
     50 
     51   By implementing the Plug API, `Plug.Builder` guarantees this module is a plug
     52   and can be handed to a web server or used as part of another pipeline.
     53 
     54   ## Overriding the default Plug API functions
     55 
     56   Both the `init/1` and `call/2` functions defined by `Plug.Builder` can be
     57   manually overridden. For example, the `init/1` function provided by
     58   `Plug.Builder` returns the options that it receives as an argument, but its
     59   behaviour can be customized:
     60 
     61       defmodule PlugWithCustomOptions do
     62         use Plug.Builder
     63         plug Plug.Logger
     64 
     65         def init(opts) do
     66           opts
     67         end
     68       end
     69 
     70   The `call/2` function that `Plug.Builder` provides is used internally to
     71   execute all the plugs listed using the `plug` macro, so overriding the
     72   `call/2` function generally implies using `super` in order to still call the
     73   plug chain:
     74 
     75       defmodule PlugWithCustomCall do
     76         use Plug.Builder
     77         plug Plug.Logger
     78         plug Plug.Head
     79 
     80         def call(conn, opts) do
     81           conn
     82           |> super(opts) # calls Plug.Logger and Plug.Head
     83           |> assign(:called_all_plugs, true)
     84         end
     85       end
     86 
     87   ## Halting a plug pipeline
     88 
     89   A plug pipeline can be halted with `Plug.Conn.halt/1`. The builder will
     90   prevent further plugs downstream from being invoked and return the current
     91   connection. In the following example, the `Plug.Logger` plug never gets
     92   called:
     93 
     94       defmodule PlugUsingHalt do
     95         use Plug.Builder
     96 
     97         plug :stopper
     98         plug Plug.Logger
     99 
    100         def stopper(conn, _opts) do
    101           halt(conn)
    102         end
    103       end
    104   """
    105 
    106   @type plug :: module | atom
    107 
    108   @doc false
    109   defmacro __using__(opts) do
    110     quote do
    111       @behaviour Plug
    112       @plug_builder_opts unquote(opts)
    113 
    114       def init(opts) do
    115         opts
    116       end
    117 
    118       def call(conn, opts) do
    119         plug_builder_call(conn, opts)
    120       end
    121 
    122       defoverridable Plug
    123 
    124       import Plug.Conn
    125       import Plug.Builder, only: [plug: 1, plug: 2, builder_opts: 0]
    126 
    127       Module.register_attribute(__MODULE__, :plugs, accumulate: true)
    128       @before_compile Plug.Builder
    129     end
    130   end
    131 
    132   @doc false
    133   defmacro __before_compile__(env) do
    134     plugs = Module.get_attribute(env.module, :plugs)
    135 
    136     plugs =
    137       if builder_ref = get_plug_builder_ref(env.module) do
    138         traverse(plugs, builder_ref)
    139       else
    140         plugs
    141       end
    142 
    143     builder_opts = Module.get_attribute(env.module, :plug_builder_opts)
    144     {conn, body} = Plug.Builder.compile(env, plugs, builder_opts)
    145 
    146     compile_time =
    147       if builder_opts[:init_mode] == :runtime do
    148         []
    149       else
    150         for triplet <- plugs,
    151             {plug, _, _} = triplet,
    152             module_plug?(plug) do
    153           quote(do: unquote(plug).__info__(:module))
    154         end
    155       end
    156 
    157     plug_builder_call =
    158       if assign = builder_opts[:copy_opts_to_assign] do
    159         quote do
    160           defp plug_builder_call(conn, opts) do
    161             unquote(conn) = Plug.Conn.assign(conn, unquote(assign), opts)
    162             unquote(body)
    163           end
    164         end
    165       else
    166         quote do
    167           defp plug_builder_call(unquote(conn), opts), do: unquote(body)
    168         end
    169       end
    170 
    171     quote do
    172       unquote_splicing(compile_time)
    173       unquote(plug_builder_call)
    174     end
    175   end
    176 
    177   defp traverse(tuple, ref) when is_tuple(tuple) do
    178     tuple |> Tuple.to_list() |> traverse(ref) |> List.to_tuple()
    179   end
    180 
    181   defp traverse(map, ref) when is_map(map) do
    182     map |> Map.to_list() |> traverse(ref) |> Map.new()
    183   end
    184 
    185   defp traverse(list, ref) when is_list(list) do
    186     Enum.map(list, &traverse(&1, ref))
    187   end
    188 
    189   defp traverse(ref, ref) do
    190     {:unquote, [], [quote(do: opts)]}
    191   end
    192 
    193   defp traverse(term, _ref) do
    194     term
    195   end
    196 
    197   @doc """
    198   A macro that stores a new plug. `opts` will be passed unchanged to the new
    199   plug.
    200 
    201   This macro doesn't add any guards when adding the new plug to the pipeline;
    202   for more information about adding plugs with guards see `compile/3`.
    203 
    204   ## Examples
    205 
    206       plug Plug.Logger               # plug module
    207       plug :foo, some_options: true  # plug function
    208 
    209   """
    210   defmacro plug(plug, opts \\ []) do
    211     # We always expand it but the @before_compile callback adds compile
    212     # time dependencies back depending on the builder's init mode.
    213     plug = expand_alias(plug, __CALLER__)
    214 
    215     # If we are sure we don't have a module plug, the options are all
    216     # runtime options too.
    217     opts =
    218       if is_atom(plug) and not module_plug?(plug) and Macro.quoted_literal?(opts) do
    219         Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
    220       else
    221         opts
    222       end
    223 
    224     quote do
    225       @plugs {unquote(plug), unquote(opts), true}
    226     end
    227   end
    228 
    229   defp expand_alias({:__aliases__, _, _} = alias, env),
    230     do: Macro.expand(alias, %{env | function: {:init, 1}})
    231 
    232   defp expand_alias(other, _env), do: other
    233 
    234   @doc """
    235   Using `builder_opts/0` is deprecated.
    236 
    237   Instead use `:copy_opts_to_assign` on `use Plug.Builder`.
    238   """
    239   # TODO: Deprecate me in future releases
    240   @doc deprecated: "Pass :copy_opts_to_assign on \"use Plug.Builder\""
    241   defmacro builder_opts() do
    242     quote do
    243       Plug.Builder.__builder_opts__(__MODULE__)
    244     end
    245   end
    246 
    247   @doc false
    248   def __builder_opts__(module) do
    249     get_plug_builder_ref(module) || generate_plug_builder_ref(module)
    250   end
    251 
    252   defp get_plug_builder_ref(module) do
    253     Module.get_attribute(module, :plug_builder_ref)
    254   end
    255 
    256   defp generate_plug_builder_ref(module) do
    257     ref = make_ref()
    258     Module.put_attribute(module, :plug_builder_ref, ref)
    259     ref
    260   end
    261 
    262   @doc """
    263   Compiles a plug pipeline.
    264 
    265   Each element of the plug pipeline (according to the type signature of this
    266   function) has the form:
    267 
    268       {plug_name, options, guards}
    269 
    270   Note that this function expects a reversed pipeline (with the last plug that
    271   has to be called coming first in the pipeline).
    272 
    273   The function returns a tuple with the first element being a quoted reference
    274   to the connection and the second element being the compiled quoted pipeline.
    275 
    276   ## Examples
    277 
    278       Plug.Builder.compile(env, [
    279         {Plug.Logger, [], true}, # no guards, as added by the Plug.Builder.plug/2 macro
    280         {Plug.Head, [], quote(do: a when is_binary(a))}
    281       ], [])
    282 
    283   """
    284   @spec compile(Macro.Env.t(), [{plug, Plug.opts(), Macro.t()}], Keyword.t()) ::
    285           {Macro.t(), Macro.t()}
    286   def compile(env, pipeline, builder_opts) do
    287     conn = quote do: conn
    288     init_mode = builder_opts[:init_mode] || :compile
    289 
    290     unless init_mode in [:compile, :runtime] do
    291       raise ArgumentError, """
    292       invalid :init_mode when compiling #{inspect(env.module)}.
    293 
    294       Supported values include :compile or :runtime. Got: #{inspect(init_mode)}
    295       """
    296     end
    297 
    298     ast =
    299       Enum.reduce(pipeline, conn, fn {plug, opts, guards}, acc ->
    300         {plug, opts, guards}
    301         |> init_plug(init_mode)
    302         |> quote_plug(init_mode, acc, env, builder_opts)
    303       end)
    304 
    305     {conn, ast}
    306   end
    307 
    308   defp module_plug?(plug), do: match?(~c"Elixir." ++ _, Atom.to_charlist(plug))
    309 
    310   # Initializes the options of a plug in the configured init_mode.
    311   defp init_plug({plug, opts, guards}, init_mode) do
    312     if module_plug?(plug) do
    313       init_module_plug(plug, opts, guards, init_mode)
    314     else
    315       init_fun_plug(plug, opts, guards)
    316     end
    317   end
    318 
    319   defp init_module_plug(plug, opts, guards, :compile) do
    320     initialized_opts = plug.init(opts)
    321 
    322     if function_exported?(plug, :call, 2) do
    323       {:module, plug, escape(initialized_opts), guards}
    324     else
    325       raise ArgumentError, "#{inspect(plug)} plug must implement call/2"
    326     end
    327   end
    328 
    329   defp init_module_plug(plug, opts, guards, :runtime) do
    330     {:module, plug, quote(do: unquote(plug).init(unquote(escape(opts)))), guards}
    331   end
    332 
    333   defp init_fun_plug(plug, opts, guards) do
    334     {:function, plug, escape(opts), guards}
    335   end
    336 
    337   defp escape(opts) do
    338     Macro.escape(opts, unquote: true)
    339   end
    340 
    341   defp quote_plug({:module, plug, opts, guards}, :compile, acc, env, builder_opts) do
    342     # Elixir v1.13/1.14 do not add a compile time dependency on require,
    343     # so we build the alias and expand it to simulate the behaviour.
    344     parts = [:"Elixir" | Enum.map(Module.split(plug), &String.to_atom/1)]
    345     alias = {:__aliases__, [line: env.line], parts}
    346     _ = Macro.expand(alias, env)
    347 
    348     quote_plug(:module, plug, opts, guards, acc, env, builder_opts)
    349   end
    350 
    351   defp quote_plug({plug_type, plug, opts, guards}, _init_mode, acc, env, builder_opts) do
    352     quote_plug(plug_type, plug, opts, guards, acc, env, builder_opts)
    353   end
    354 
    355   # `acc` is a series of nested plug calls in the form of plug3(plug2(plug1(conn))).
    356   # `quote_plug` wraps a new plug around that series of calls.
    357   defp quote_plug(plug_type, plug, opts, guards, acc, env, builder_opts) do
    358     call = quote_plug_call(plug_type, plug, opts)
    359 
    360     error_message =
    361       case plug_type do
    362         :module -> "expected #{inspect(plug)}.call/2 to return a Plug.Conn"
    363         :function -> "expected #{plug}/2 to return a Plug.Conn"
    364       end <> ", all plugs must receive a connection (conn) and return a connection"
    365 
    366     quote generated: true do
    367       case unquote(compile_guards(call, guards)) do
    368         %Plug.Conn{halted: true} = conn ->
    369           unquote(log_halt(plug_type, plug, env, builder_opts))
    370           conn
    371 
    372         %Plug.Conn{} = conn ->
    373           unquote(acc)
    374 
    375         other ->
    376           raise unquote(error_message) <> ", got: #{inspect(other)}"
    377       end
    378     end
    379   end
    380 
    381   defp quote_plug_call(:function, plug, opts) do
    382     quote do: unquote(plug)(conn, unquote(opts))
    383   end
    384 
    385   defp quote_plug_call(:module, plug, opts) do
    386     quote do: unquote(plug).call(conn, unquote(opts))
    387   end
    388 
    389   defp compile_guards(call, true) do
    390     call
    391   end
    392 
    393   defp compile_guards(call, guards) do
    394     quote do
    395       case true do
    396         true when unquote(guards) -> unquote(call)
    397         true -> conn
    398       end
    399     end
    400   end
    401 
    402   defp log_halt(plug_type, plug, env, builder_opts) do
    403     if level = builder_opts[:log_on_halt] do
    404       message =
    405         case plug_type do
    406           :module -> "#{inspect(env.module)} halted in #{inspect(plug)}.call/2"
    407           :function -> "#{inspect(env.module)} halted in #{inspect(plug)}/2"
    408         end
    409 
    410       quote do
    411         require Logger
    412         # Matching, to make Dialyzer happy on code executing Plug.Builder.compile/3
    413         _ = Logger.unquote(level)(unquote(message))
    414       end
    415     else
    416       nil
    417     end
    418   end
    419 end