plug.ex (4989B)
1 defmodule Plug do 2 @moduledoc """ 3 The plug specification. 4 5 There are two kind of plugs: function plugs and module plugs. 6 7 #### Function plugs 8 9 A function plug is any function that receives a connection and a set of 10 options and returns a connection. Its type signature must be: 11 12 (Plug.Conn.t, Plug.opts) :: Plug.Conn.t 13 14 #### Module plugs 15 16 A module plug is an extension of the function plug. It is a module that must 17 export: 18 19 * a `c:call/2` function with the signature defined above 20 * an `c:init/1` function which takes a set of options and initializes it. 21 22 The result returned by `c:init/1` is passed as second argument to `c:call/2`. Note 23 that `c:init/1` may be called during compilation and as such it must not return 24 pids, ports or values that are specific to the runtime. 25 26 The API expected by a module plug is defined as a behaviour by the 27 `Plug` module (this module). 28 29 ## Examples 30 31 Here's an example of a function plug: 32 33 def json_header_plug(conn, _opts) do 34 Plug.Conn.put_resp_content_type(conn, "application/json") 35 end 36 37 Here's an example of a module plug: 38 39 defmodule JSONHeaderPlug do 40 import Plug.Conn 41 42 def init(opts) do 43 opts 44 end 45 46 def call(conn, _opts) do 47 put_resp_content_type(conn, "application/json") 48 end 49 end 50 51 ## The Plug pipeline 52 53 The `Plug.Builder` module provides conveniences for building plug 54 pipelines. 55 """ 56 57 @type opts :: 58 binary 59 | tuple 60 | atom 61 | integer 62 | float 63 | [opts] 64 | %{optional(opts) => opts} 65 | MapSet.t() 66 67 @callback init(opts) :: opts 68 @callback call(conn :: Plug.Conn.t(), opts) :: Plug.Conn.t() 69 70 require Logger 71 72 @doc """ 73 Run a series of Plugs at runtime. 74 75 The plugs given here can be either a tuple, representing a module plug 76 and their options, or a simple function that receives a connection and 77 returns a connection. 78 79 If any of the plugs halt, the remaining plugs are not invoked. If the 80 given connection was already halted, none of the plugs are invoked 81 either. 82 83 While `Plug.Builder` works at compile-time, this is a straight-forward 84 alternative that works at runtime. 85 86 ## Examples 87 88 Plug.run(conn, [{Plug.Head, []}, &IO.inspect/1]) 89 90 ## Options 91 92 * `:log_on_halt` - a log level to be used if a Plug halts 93 94 """ 95 @spec run(Plug.Conn.t(), [{module, opts} | (Plug.Conn.t() -> Plug.Conn.t())], Keyword.t()) :: 96 Plug.Conn.t() 97 def run(conn, plugs, opts \\ []) 98 99 def run(%Plug.Conn{halted: true} = conn, _plugs, _opts), 100 do: conn 101 102 def run(%Plug.Conn{} = conn, plugs, opts), 103 do: do_run(conn, plugs, Keyword.get(opts, :log_on_halt)) 104 105 defp do_run(conn, [{mod, opts} | plugs], level) when is_atom(mod) do 106 case mod.call(conn, mod.init(opts)) do 107 %Plug.Conn{halted: true} = conn -> 108 level && Logger.log(level, "Plug halted in #{inspect(mod)}.call/2") 109 conn 110 111 %Plug.Conn{} = conn -> 112 do_run(conn, plugs, level) 113 114 other -> 115 raise "expected #{inspect(mod)} to return Plug.Conn, got: #{inspect(other)}" 116 end 117 end 118 119 defp do_run(conn, [fun | plugs], level) when is_function(fun, 1) do 120 case fun.(conn) do 121 %Plug.Conn{halted: true} = conn -> 122 level && Logger.log(level, "Plug halted in #{inspect(fun)}") 123 conn 124 125 %Plug.Conn{} = conn -> 126 do_run(conn, plugs, level) 127 128 other -> 129 raise "expected #{inspect(fun)} to return Plug.Conn, got: #{inspect(other)}" 130 end 131 end 132 133 defp do_run(conn, [], _level), do: conn 134 135 @doc """ 136 Forwards requests to another Plug setting the connection to a trailing subpath of the request. 137 138 The `path_info` on the forwarded connection will only include the trailing segments 139 of the request path supplied to forward, while `conn.script_name` will 140 retain the correct base path for e.g. url generation. 141 142 ## Example 143 144 defmodule Router do 145 def init(opts), do: opts 146 147 def call(conn, opts) do 148 case conn do 149 # Match subdomain 150 %{host: "admin." <> _} -> 151 AdminRouter.call(conn, opts) 152 153 # Match path on localhost 154 %{host: "localhost", path_info: ["admin" | rest]} -> 155 Plug.forward(conn, rest, AdminRouter, opts) 156 157 _ -> 158 MainRouter.call(conn, opts) 159 end 160 end 161 end 162 163 """ 164 @spec forward(Plug.Conn.t(), [String.t()], atom, Plug.opts()) :: Plug.Conn.t() 165 def forward(%Plug.Conn{path_info: path, script_name: script} = conn, new_path, target, opts) do 166 {base, split_path} = Enum.split(path, length(path) - length(new_path)) 167 168 conn = do_forward(target, %{conn | path_info: split_path, script_name: script ++ base}, opts) 169 %{conn | path_info: path, script_name: script} 170 end 171 172 defp do_forward({mod, fun}, conn, opts), do: apply(mod, fun, [conn, opts]) 173 defp do_forward(mod, conn, opts), do: mod.call(conn, opts) 174 end