utils.ex (8564B)
1 defmodule Plug.Router.InvalidSpecError do 2 defexception message: "invalid route specification" 3 end 4 5 defmodule Plug.Router.MalformedURIError do 6 defexception message: "malformed URI", plug_status: 400 7 end 8 9 defmodule Plug.Router.Utils do 10 @moduledoc false 11 12 @doc """ 13 Decodes path information for dispatching. 14 """ 15 def decode_path_info!(conn) do 16 # TODO: Remove rescue as this can't fail from Elixir v1.13 17 try do 18 Enum.map(conn.path_info, &URI.decode/1) 19 rescue 20 e in ArgumentError -> 21 reason = %Plug.Router.MalformedURIError{message: e.message} 22 Plug.Conn.WrapperError.reraise(conn, :error, reason, __STACKTRACE__) 23 end 24 end 25 26 @doc """ 27 Converts a given method to its connection representation. 28 29 The request method is stored in the `Plug.Conn` struct as an uppercase string 30 (like `"GET"` or `"POST"`). This function converts `method` to that 31 representation. 32 33 ## Examples 34 35 iex> Plug.Router.Utils.normalize_method(:get) 36 "GET" 37 38 """ 39 def normalize_method(method) do 40 method |> to_string |> String.upcase() 41 end 42 43 @doc ~S""" 44 Builds the pattern that will be used to match against the request's host 45 (provided via the `:host`) option. 46 47 If `host` is `nil`, a wildcard match (`_`) will be returned. If `host` ends 48 with a dot, a match like `"host." <> _` will be returned. 49 50 ## Examples 51 52 iex> Plug.Router.Utils.build_host_match(nil) 53 {:_, [], Plug.Router.Utils} 54 55 iex> Plug.Router.Utils.build_host_match("foo.com") 56 "foo.com" 57 58 iex> "api." |> Plug.Router.Utils.build_host_match() |> Macro.to_string() 59 "\"api.\" <> _" 60 61 """ 62 def build_host_match(host) do 63 cond do 64 is_nil(host) -> quote do: _ 65 String.last(host) == "." -> quote do: unquote(host) <> _ 66 is_binary(host) -> host 67 end 68 end 69 70 @doc """ 71 Generates a representation that will only match routes 72 according to the given `spec`. 73 74 If a non-binary spec is given, it is assumed to be 75 custom match arguments and they are simply returned. 76 77 ## Examples 78 79 iex> Plug.Router.Utils.build_path_match("/foo/:id") 80 {[:id], ["foo", {:id, [], nil}]} 81 82 """ 83 def build_path_match(path, context \\ nil) when is_binary(path) do 84 case build_path_clause(path, true, context) do 85 {params, match, true, _post_match} -> 86 {Enum.map(params, &String.to_atom(&1)), match} 87 88 {_, _, _, _} -> 89 raise Plug.Router.InvalidSpecError, 90 "invalid dynamic path. Only letters, numbers, and underscore are allowed after : in " <> 91 inspect(path) 92 end 93 end 94 95 @doc """ 96 Builds a list of path param names and var match pairs. 97 98 This is used to build parameter maps from existing variables. 99 Excludes variables with underscore. 100 101 ## Examples 102 103 iex> Plug.Router.Utils.build_path_params_match(["id"]) 104 [{"id", {:id, [], nil}}] 105 iex> Plug.Router.Utils.build_path_params_match(["_id"]) 106 [] 107 108 iex> Plug.Router.Utils.build_path_params_match([:id]) 109 [{"id", {:id, [], nil}}] 110 iex> Plug.Router.Utils.build_path_params_match([:_id]) 111 [] 112 113 """ 114 # TODO: Make me private in Plug v2.0 115 def build_path_params_match(params, context \\ nil) 116 117 def build_path_params_match([param | _] = params, context) when is_binary(param) do 118 params 119 |> Enum.reject(&match?("_" <> _, &1)) 120 |> Enum.map(&{&1, Macro.var(String.to_atom(&1), context)}) 121 end 122 123 def build_path_params_match([param | _] = params, context) when is_atom(param) do 124 params 125 |> Enum.map(&{Atom.to_string(&1), Macro.var(&1, context)}) 126 |> Enum.reject(&match?({"_" <> _var, _macro}, &1)) 127 end 128 129 def build_path_params_match([], _context) do 130 [] 131 end 132 133 @doc """ 134 Builds a clause with match, guards, and post matches, 135 including the known parameters. 136 """ 137 def build_path_clause(path, guard, context \\ nil) when is_binary(path) do 138 compiled = :binary.compile_pattern([":", "*"]) 139 140 {params, match, guards, post_match} = 141 path 142 |> split() 143 |> build_path_clause([], [], [], [], context, compiled) 144 145 if guard != true and guards != [] do 146 raise ArgumentError, "cannot use \"when\" guards in route when using suffix matches" 147 end 148 149 params = params |> Enum.uniq() |> Enum.reverse() 150 guards = Enum.reduce(guards, guard, "e(do: unquote(&1) and unquote(&2))) 151 {params, match, guards, post_match} 152 end 153 154 defp build_path_clause([segment | rest], params, match, guards, post_match, context, compiled) do 155 case :binary.matches(segment, compiled) do 156 [] -> 157 build_path_clause(rest, params, [segment | match], guards, post_match, context, compiled) 158 159 [{prefix_size, _}] -> 160 suffix_size = byte_size(segment) - prefix_size - 1 161 <<prefix::binary-size(prefix_size), char, suffix::binary-size(suffix_size)>> = segment 162 {param, suffix} = parse_suffix(suffix) 163 params = [param | params] 164 var = Macro.var(String.to_atom(param), context) 165 166 case char do 167 ?* when suffix != "" -> 168 raise Plug.Router.InvalidSpecError, 169 "globs (*var) cannot be followed by suffixes, got: #{inspect(segment)}" 170 171 ?* when rest != [] -> 172 raise Plug.Router.InvalidSpecError, 173 "globs (*var) must always be in the last path, got glob in: #{inspect(segment)}" 174 175 ?* -> 176 submatch = 177 if prefix != "" do 178 IO.warn(""" 179 doing a prefix match with globs is deprecated, invalid segment #{inspect(segment)}. 180 181 You can either replace by a single segment match: 182 183 /foo/bar-:var 184 185 Or by mixing single segment match with globs: 186 187 /foo/bar-:var/*rest 188 """) 189 190 quote do: [unquote(prefix) <> _ | _] = unquote(var) 191 else 192 var 193 end 194 195 match = 196 case match do 197 [] -> 198 submatch 199 200 [last | match] -> 201 Enum.reverse([quote(do: unquote(last) | unquote(submatch)) | match]) 202 end 203 204 {params, match, guards, post_match} 205 206 ?: -> 207 match = 208 if prefix == "", 209 do: [var | match], 210 else: [quote(do: unquote(prefix) <> unquote(var)) | match] 211 212 {post_match, guards} = 213 if suffix == "" do 214 {post_match, guards} 215 else 216 guard = 217 quote do 218 binary_part( 219 unquote(var), 220 byte_size(unquote(var)) - unquote(byte_size(suffix)), 221 unquote(byte_size(suffix)) 222 ) == unquote(suffix) 223 end 224 225 trim = 226 quote do 227 unquote(var) = String.trim_trailing(unquote(var), unquote(suffix)) 228 end 229 230 {[trim | post_match], [guard | guards]} 231 end 232 233 build_path_clause(rest, params, match, guards, post_match, context, compiled) 234 end 235 236 [_ | _] -> 237 raise Plug.Router.InvalidSpecError, 238 "only one dynamic entry (:var or *glob) per path segment is allowed, got: " <> 239 inspect(segment) 240 end 241 end 242 243 defp build_path_clause([], params, match, guards, post_match, _context, _compiled) do 244 {params, Enum.reverse(match), guards, post_match} 245 end 246 247 defp parse_suffix(<<h, t::binary>>) when h in ?a..?z or h == ?_, 248 do: parse_suffix(t, <<h>>) 249 250 defp parse_suffix(suffix) do 251 raise Plug.Router.InvalidSpecError, 252 "invalid dynamic path. The characters : and * must be immediately followed by " <> 253 "lowercase letters or underscore, got: :#{suffix}" 254 end 255 256 defp parse_suffix(<<h, t::binary>>, acc) 257 when h in ?a..?z or h in ?A..?Z or h in ?0..?9 or h == ?_, 258 do: parse_suffix(t, <<acc::binary, h>>) 259 260 defp parse_suffix(rest, acc), 261 do: {acc, rest} 262 263 @doc """ 264 Splits the given path into several segments. 265 It ignores both leading and trailing slashes in the path. 266 267 ## Examples 268 269 iex> Plug.Router.Utils.split("/foo/bar") 270 ["foo", "bar"] 271 272 iex> Plug.Router.Utils.split("/:id/*") 273 [":id", "*"] 274 275 iex> Plug.Router.Utils.split("/foo//*_bar") 276 ["foo", "*_bar"] 277 278 """ 279 def split(bin) do 280 for segment <- String.split(bin, "/"), segment != "", do: segment 281 end 282 283 @deprecated "Use Plug.forward/4 instead" 284 defdelegate forward(conn, new_path, target, opts), to: Plug 285 end