/
builder.ex
402 lines (307 loc) · 10.9 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
defmodule Plug.Builder do
@moduledoc """
Conveniences for building plugs.
This module can be `use`-d into a module in order 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
Multiple plugs can be defined with the `plug/2` macro, forming a pipeline.
The plugs in the pipeline will be executed in the order they've been added
through the `plug/2` macro. In the example above, `Plug.Logger` will be
called first and then the `:hello` function plug will be called on the
resulting connection.
`Plug.Builder` also imports the `Plug.Conn` module, making functions like
`send_resp/3` available.
## Options
When used, the following options are accepted by `Plug.Builder`:
* `:log_on_halt` - accepts the level to log whenever the request is halted
* `:init_mode` - the environment to initialize the plug's options, one of
`:compile` or `:runtime`. Defaults `:compile`.
## Plug behaviour
Internally, `Plug.Builder` implements the `Plug` behaviour, which means both
the `init/1` and `call/2` functions are defined.
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.
## 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
A plug pipeline can be halted with `Plug.Conn.halt/1`. The builder will
prevent further plugs downstream from being invoked and return 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)
quote do
defp plug_builder_call(unquote(conn), opts), do: unquote(body)
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
plug = Macro.expand(plug, %{__CALLER__ | function: {:init, 1}})
quote do
@plugs {unquote(plug), unquote(opts), true}
end
end
@doc """
Annotates a plug will receive the options given
to the current module itself as arguments.
Imagine the following plug:
defmodule MyPlug do
use Plug.Builder
plug :inspect_opts, builder_opts()
defp inspect_opts(conn, opts) do
IO.inspect(opts)
conn
end
end
When plugged as:
plug MyPlug, custom: :options
It will print `[custom: :options]` as the builder options
were passed to the inner plug.
Note you only pass `builder_opts()` to **function plugs**.
You cannot use `builder_opts()` with module plugs because
their options are evaluated at compile time. If you need
to pass `builder_opts()` to a module plug, you can wrap
the module plug in function. To be precise, do not do this:
plug Plug.Parsers, builder_opts()
Instead do this:
plug :custom_plug_parsers, builder_opts()
defp custom_plug_parsers(conn, opts) do
Plug.Parsers.call(conn, Plug.Parsers.init(opts))
end
"""
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
# Initializes the options of a plug in the configured init_mode.
defp init_plug({plug, opts, guards}, init_mode) do
case Atom.to_charlist(plug) do
~c"Elixir." ++ _ -> init_module_plug(plug, opts, guards, init_mode)
_ -> 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
call = quote_plug(:module, plug, opts, guards, acc, env, builder_opts)
quote do
require unquote(plug)
unquote(call)
end
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