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