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