176 lines
5.0 KiB
Elixir
176 lines
5.0 KiB
Elixir
|
defmodule Plug do
|
||
|
@moduledoc """
|
||
|
The plug specification.
|
||
|
|
||
|
## Types of plugs
|
||
|
|
||
|
There are two kind of plugs: function plugs and module plugs.
|
||
|
|
||
|
### Function plugs
|
||
|
|
||
|
A function plug is by definition any function that receives a connection
|
||
|
and a set of options and returns a connection. Function plugs must have
|
||
|
the following type signature:
|
||
|
|
||
|
(Plug.Conn.t, Plug.opts) :: Plug.Conn.t
|
||
|
|
||
|
### Module plugs
|
||
|
|
||
|
A module plug is an extension of the function plug. It is a module that must
|
||
|
export:
|
||
|
|
||
|
* a `c:call/2` function with the signature defined above
|
||
|
* an `c:init/1` function which takes a set of options and initializes it.
|
||
|
|
||
|
The result returned by `c:init/1` is passed as second argument to `c:call/2`. Note
|
||
|
that `c:init/1` may be called during compilation and as such it must not return
|
||
|
pids, ports or values that are specific to the runtime.
|
||
|
|
||
|
The API expected by a module plug is defined as a behaviour by the
|
||
|
`Plug` module (this module).
|
||
|
|
||
|
## Examples
|
||
|
|
||
|
Here's an example of a function plug:
|
||
|
|
||
|
def json_header_plug(conn, _opts) do
|
||
|
Plug.Conn.put_resp_content_type(conn, "application/json")
|
||
|
end
|
||
|
|
||
|
Here's an example of a module plug:
|
||
|
|
||
|
defmodule JSONHeaderPlug do
|
||
|
import Plug.Conn
|
||
|
|
||
|
def init(opts) do
|
||
|
opts
|
||
|
end
|
||
|
|
||
|
def call(conn, _opts) do
|
||
|
put_resp_content_type(conn, "application/json")
|
||
|
end
|
||
|
end
|
||
|
|
||
|
## The Plug pipeline
|
||
|
|
||
|
The `Plug.Builder` module provides conveniences for building plug pipelines.
|
||
|
"""
|
||
|
|
||
|
@type opts ::
|
||
|
binary
|
||
|
| tuple
|
||
|
| atom
|
||
|
| integer
|
||
|
| float
|
||
|
| [opts]
|
||
|
| %{optional(opts) => opts}
|
||
|
| MapSet.t()
|
||
|
|
||
|
@callback init(opts) :: opts
|
||
|
@callback call(conn :: Plug.Conn.t(), opts) :: Plug.Conn.t()
|
||
|
|
||
|
require Logger
|
||
|
|
||
|
@doc """
|
||
|
Run a series of plugs at runtime.
|
||
|
|
||
|
The plugs given here can be either a tuple, representing a module plug
|
||
|
and their options, or a simple function that receives a connection and
|
||
|
returns a connection.
|
||
|
|
||
|
If any plug halts, the connection won't invoke the remaining plugs. If the
|
||
|
given connection was already halted, none of the plugs are invoked either.
|
||
|
|
||
|
While `Plug.Builder` is designed to operate at compile-time, the `run` function
|
||
|
serves as a straightforward alternative for runtime executions.
|
||
|
|
||
|
## Examples
|
||
|
|
||
|
Plug.run(conn, [{Plug.Head, []}, &IO.inspect/1])
|
||
|
|
||
|
## Options
|
||
|
|
||
|
* `:log_on_halt` - a log level to be used if a plug halts
|
||
|
|
||
|
"""
|
||
|
@spec run(Plug.Conn.t(), [{module, opts} | (Plug.Conn.t() -> Plug.Conn.t())], Keyword.t()) ::
|
||
|
Plug.Conn.t()
|
||
|
def run(conn, plugs, opts \\ [])
|
||
|
|
||
|
def run(%Plug.Conn{halted: true} = conn, _plugs, _opts),
|
||
|
do: conn
|
||
|
|
||
|
def run(%Plug.Conn{} = conn, plugs, opts),
|
||
|
do: do_run(conn, plugs, Keyword.get(opts, :log_on_halt))
|
||
|
|
||
|
defp do_run(conn, [{mod, opts} | plugs], level) when is_atom(mod) do
|
||
|
case mod.call(conn, mod.init(opts)) do
|
||
|
%Plug.Conn{halted: true} = conn ->
|
||
|
level && Logger.log(level, "Plug halted in #{inspect(mod)}.call/2")
|
||
|
conn
|
||
|
|
||
|
%Plug.Conn{} = conn ->
|
||
|
do_run(conn, plugs, level)
|
||
|
|
||
|
other ->
|
||
|
raise "expected #{inspect(mod)} to return Plug.Conn, got: #{inspect(other)}"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defp do_run(conn, [fun | plugs], level) when is_function(fun, 1) do
|
||
|
case fun.(conn) do
|
||
|
%Plug.Conn{halted: true} = conn ->
|
||
|
level && Logger.log(level, "Plug halted in #{inspect(fun)}")
|
||
|
conn
|
||
|
|
||
|
%Plug.Conn{} = conn ->
|
||
|
do_run(conn, plugs, level)
|
||
|
|
||
|
other ->
|
||
|
raise "expected #{inspect(fun)} to return Plug.Conn, got: #{inspect(other)}"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defp do_run(conn, [], _level), do: conn
|
||
|
|
||
|
@doc """
|
||
|
Forwards requests to another plug while setting the connection to a trailing subpath of the request.
|
||
|
|
||
|
The `path_info` on the forwarded connection will only include the request path trailing segments
|
||
|
supplied to the `forward` function. The `conn.script_name` attribute retains the correct base path,
|
||
|
e.g., url generation.
|
||
|
|
||
|
## Example
|
||
|
|
||
|
defmodule Router do
|
||
|
def init(opts), do: opts
|
||
|
|
||
|
def call(conn, opts) do
|
||
|
case conn do
|
||
|
# Match subdomain
|
||
|
%{host: "admin." <> _} ->
|
||
|
AdminRouter.call(conn, opts)
|
||
|
|
||
|
# Match path on localhost
|
||
|
%{host: "localhost", path_info: ["admin" | rest]} ->
|
||
|
Plug.forward(conn, rest, AdminRouter, opts)
|
||
|
|
||
|
_ ->
|
||
|
MainRouter.call(conn, opts)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
"""
|
||
|
@spec forward(Plug.Conn.t(), [String.t()], atom, Plug.opts()) :: Plug.Conn.t()
|
||
|
def forward(%Plug.Conn{path_info: path, script_name: script} = conn, new_path, target, opts) do
|
||
|
{base, split_path} = Enum.split(path, length(path) - length(new_path))
|
||
|
|
||
|
conn = do_forward(target, %{conn | path_info: split_path, script_name: script ++ base}, opts)
|
||
|
%{conn | path_info: path, script_name: script}
|
||
|
end
|
||
|
|
||
|
defp do_forward({mod, fun}, conn, opts), do: apply(mod, fun, [conn, opts])
|
||
|
defp do_forward(mod, conn, opts), do: mod.call(conn, opts)
|
||
|
end
|