zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

router.ex (20450B)


      1 defmodule Plug.Router do
      2   @moduledoc ~S"""
      3   A DSL to define a routing algorithm that works with Plug.
      4 
      5   It provides a set of macros to generate routes. For example:
      6 
      7       defmodule AppRouter do
      8         use Plug.Router
      9 
     10         plug :match
     11         plug :dispatch
     12 
     13         get "/hello" do
     14           send_resp(conn, 200, "world")
     15         end
     16 
     17         match _ do
     18           send_resp(conn, 404, "oops")
     19         end
     20       end
     21 
     22   Each route receives a `conn` variable containing a `Plug.Conn`
     23   struct and it needs to return a connection, as per the Plug spec.
     24   A catch-all `match` is recommended to be defined as in the example
     25   above, otherwise routing fails with a function clause error.
     26 
     27   The router is itself a plug, which means it can be invoked as:
     28 
     29       AppRouter.call(conn, AppRouter.init([]))
     30 
     31   Each `Plug.Router` has a plug pipeline, defined by `Plug.Builder`,
     32   and by default it requires two plugs: `:match` and `:dispatch`.
     33   `:match` is responsible for finding a matching route which is
     34   then forwarded to `:dispatch`. This means users can easily hook
     35   into the router mechanism and add behaviour before match, before
     36   dispatch, or after both. See the `Plug.Builder` module for more
     37   information.
     38 
     39   ## Routes
     40 
     41       get "/hello" do
     42         send_resp(conn, 200, "world")
     43       end
     44 
     45   In the example above, a request will only match if it is a `GET`
     46   request and the route is "/hello". The supported HTTP methods are
     47   `get`, `post`, `put`, `patch`, `delete` and `options`.
     48 
     49   A route can also specify parameters which will then be available
     50   in the function body:
     51 
     52       get "/hello/:name" do
     53         send_resp(conn, 200, "hello #{name}")
     54       end
     55 
     56   This means the name can also be used in guards:
     57 
     58       get "/hello/:name" when name in ~w(foo bar) do
     59         send_resp(conn, 200, "hello #{name}")
     60       end
     61 
     62   The `:name` parameter will also be available in the function body as
     63   `conn.params["name"]` and `conn.path_params["name"]`.
     64 
     65   The identifier always starts with `:` and must be followed by letters,
     66   numbers, and underscores, like any Elixir variable. It is possible for
     67   identifiers to be either prefixed or suffixed by other words. For example,
     68   you can include a suffix such as a dot delimited file extension:
     69 
     70       get "/hello/:name.json" do
     71         send_resp(conn, 200, "hello #{name}")
     72       end
     73 
     74   The above will match `/hello/foo.json` but not `/hello/foo`.
     75   Other delimiters such as `-`, `@` may be used to denote suffixes.
     76 
     77   Routes allow for globbing which will match the remaining parts
     78   of a route. A glob match is done with the `*` character followed
     79   by the variable name. Typically you prefix the variable name with
     80   underscore to discard it:
     81 
     82       get "/hello/*_rest" do
     83         send_resp(conn, 200, "matches all routes starting with /hello")
     84       end
     85 
     86   But you can also assign the glob to any variable. The contents will
     87   always be a list:
     88 
     89       get "/hello/*glob" do
     90         send_resp(conn, 200, "route after /hello: #{inspect glob}")
     91       end
     92 
     93   Opposite to `:identifiers`, globs do not allow prefix nor suffix
     94   matches.
     95 
     96   Finally, a general `match` function is also supported:
     97 
     98       match "/hello" do
     99         send_resp(conn, 200, "world")
    100       end
    101 
    102   A `match` will match any route regardless of the HTTP method.
    103   Check `match/3` for more information on how route compilation
    104   works and a list of supported options.
    105 
    106   ## Parameter Parsing
    107 
    108   Handling request data can be done through the
    109   [`Plug.Parsers`](https://hexdocs.pm/plug/Plug.Parsers.html#content) plug. It
    110   provides support for parsing URL-encoded, form-data, and JSON data as well as
    111   providing a behaviour that others parsers can adopt.
    112 
    113   Here is an example of `Plug.Parsers` can be used in a `Plug.Router` router to
    114   parse the JSON-encoded body of a POST request:
    115 
    116       defmodule AppRouter do
    117         use Plug.Router
    118 
    119         plug :match
    120 
    121         plug Plug.Parsers,
    122              parsers: [:json],
    123              pass:  ["application/json"],
    124              json_decoder: Jason
    125 
    126         plug :dispatch
    127 
    128         post "/hello" do
    129           IO.inspect conn.body_params # Prints JSON POST body
    130           send_resp(conn, 200, "Success!")
    131         end
    132       end
    133 
    134   It is important that `Plug.Parsers` is placed before the `:dispatch` plug in
    135   the pipeline, otherwise the matched clause route will not receive the parsed
    136   body in its `Plug.Conn` argument when dispatched.
    137 
    138   `Plug.Parsers` can also be plugged between `:match` and `:dispatch` (like in
    139   the example above): this means that `Plug.Parsers` will run only if there is a
    140   matching route. This can be useful to perform actions such as authentication
    141   *before* parsing the body, which should only be parsed if a route matches
    142   afterwards.
    143 
    144   ## Error handling
    145 
    146   In case something goes wrong in a request, the router by default
    147   will crash, without returning any response to the client. This
    148   behaviour can be configured in two ways, by using two different
    149   modules:
    150 
    151   * `Plug.ErrorHandler` - allows the developer to customize exactly
    152     which page is sent to the client via the `handle_errors/2` function;
    153 
    154   * `Plug.Debugger` - automatically shows debugging and request information
    155     about the failure. This module is recommended to be used only in a
    156     development environment.
    157 
    158   Here is an example of how both modules could be used in an application:
    159 
    160       defmodule AppRouter do
    161         use Plug.Router
    162 
    163         if Mix.env == :dev do
    164           use Plug.Debugger
    165         end
    166 
    167         use Plug.ErrorHandler
    168 
    169         plug :match
    170         plug :dispatch
    171 
    172         get "/hello" do
    173           send_resp(conn, 200, "world")
    174         end
    175 
    176         defp handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do
    177           send_resp(conn, conn.status, "Something went wrong")
    178         end
    179       end
    180 
    181   ## Passing data between routes and plugs
    182 
    183   It is also possible to assign data to the `Plug.Conn` that will
    184   be available to any plug invoked after the `:match` plug.
    185   This is very useful if you want a matched route to customize how
    186   later plugs will behave.
    187 
    188   You can use `:assigns` (which contains user data) or `:private`
    189   (which contains library/framework data) for this. For example:
    190 
    191       get "/hello", assigns: %{an_option: :a_value} do
    192         send_resp(conn, 200, "world")
    193       end
    194 
    195   In the example above, `conn.assigns[:an_option]` will be available
    196   to all plugs invoked after `:match`. Such plugs can read from
    197   `conn.assigns` (or `conn.private`) to configure their behaviour
    198   based on the matched route.
    199 
    200   ## `use` options
    201 
    202   All of the options given to `use Plug.Router` are forwarded to
    203   `Plug.Builder`. See the `Plug.Builder` module for more information.
    204 
    205   ## Telemetry
    206 
    207   The router emits the following telemetry events:
    208 
    209     * `[:plug, :router_dispatch, :start]` - dispatched before dispatching to a matched route
    210       * Measurement: `%{system_time: System.system_time}`
    211       * Metadata: `%{telemetry_span_context: term(), conn: Plug.Conn.t, route: binary, router: module}`
    212 
    213     * `[:plug, :router_dispatch, :exception]` - dispatched after exceptions on dispatching a route
    214       * Measurement: `%{duration: native_time}`
    215       * Metadata: `%{telemetry_span_context: term(), conn: Plug.Conn.t, route: binary, router: module, kind: :throw | :error | :exit, reason: term(), stacktrace: list()}`
    216 
    217     * `[:plug, :router_dispatch, :stop]` - dispatched after successfully dispatching a matched route
    218       * Measurement: `%{duration: native_time}`
    219       * Metadata: `%{telemetry_span_context: term(), conn: Plug.Conn.t, route: binary, router: module}`
    220 
    221   """
    222 
    223   @doc false
    224   defmacro __using__(opts) do
    225     quote location: :keep do
    226       import Plug.Router
    227       @plug_router_to %{}
    228       @before_compile Plug.Router
    229 
    230       use Plug.Builder, unquote(opts)
    231 
    232       @doc false
    233       def match(conn, _opts) do
    234         do_match(conn, conn.method, Plug.Router.Utils.decode_path_info!(conn), conn.host)
    235       end
    236 
    237       @doc false
    238       def dispatch(%Plug.Conn{} = conn, opts) do
    239         {path, fun} = Map.fetch!(conn.private, :plug_route)
    240 
    241         try do
    242           :telemetry.span(
    243             [:plug, :router_dispatch],
    244             %{conn: conn, route: path, router: __MODULE__},
    245             fn ->
    246               conn = fun.(conn, opts)
    247               {conn, %{conn: conn, route: path, router: __MODULE__}}
    248             end
    249           )
    250         catch
    251           kind, reason ->
    252             Plug.Conn.WrapperError.reraise(conn, kind, reason, __STACKTRACE__)
    253         end
    254       end
    255 
    256       defoverridable match: 2, dispatch: 2
    257     end
    258   end
    259 
    260   @doc false
    261   defmacro __before_compile__(env) do
    262     unless Module.defines?(env.module, {:do_match, 4}) do
    263       raise "no routes defined in module #{inspect(env.module)} using Plug.Router"
    264     end
    265 
    266     router_to = Module.get_attribute(env.module, :plug_router_to)
    267     init_mode = Module.get_attribute(env.module, :plug_builder_opts)[:init_mode]
    268 
    269     defs =
    270       for {callback, {mod, opts}} <- router_to do
    271         if init_mode == :runtime do
    272           quote do
    273             defp unquote(callback)(conn, _opts) do
    274               unquote(mod).call(conn, unquote(mod).init(unquote(Macro.escape(opts))))
    275             end
    276           end
    277         else
    278           opts = mod.init(opts)
    279 
    280           quote do
    281             defp unquote(callback)(conn, _opts) do
    282               require unquote(mod)
    283               unquote(mod).call(conn, unquote(Macro.escape(opts)))
    284             end
    285           end
    286         end
    287       end
    288 
    289     quote do
    290       unquote_splicing(defs)
    291       import Plug.Router, only: []
    292     end
    293   end
    294 
    295   @doc """
    296   Returns the path of the route that the request was matched to.
    297   """
    298   @spec match_path(Plug.Conn.t()) :: String.t()
    299   def match_path(%Plug.Conn{} = conn) do
    300     {path, _fun} = Map.fetch!(conn.private, :plug_route)
    301     path
    302   end
    303 
    304   ## Match
    305 
    306   @doc """
    307   Main API to define routes.
    308 
    309   It accepts an expression representing the path and many options
    310   allowing the match to be configured.
    311 
    312   The route can dispatch either to a function body or a Plug module.
    313 
    314   ## Examples
    315 
    316       match "/foo/bar", via: :get do
    317         send_resp(conn, 200, "hello world")
    318       end
    319 
    320       match "/baz", to: MyPlug, init_opts: [an_option: :a_value]
    321 
    322   ## Options
    323 
    324   `match/3` and the other route macros accept the following options:
    325 
    326     * `:host` - the host which the route should match. Defaults to `nil`,
    327       meaning no host match, but can be a string like "example.com" or a
    328       string ending with ".", like "subdomain." for a subdomain match.
    329 
    330     * `:private` - assigns values to `conn.private` for use in the match
    331 
    332     * `:assigns` - assigns values to `conn.assigns` for use in the match
    333 
    334     * `:via` - matches the route against some specific HTTP method(s) specified
    335       as an atom, like `:get` or `:put`, or a list, like `[:get, :post]`.
    336 
    337     * `:do` - contains the implementation to be invoked in case
    338       the route matches.
    339 
    340     * `:to` - a Plug that will be called in case the route matches.
    341 
    342     * `:init_opts` - the options for the target Plug given by `:to`.
    343 
    344   A route should specify only one of `:do` or `:to` options.
    345   """
    346   defmacro match(path, options, contents \\ []) do
    347     compile(nil, path, options, contents, __CALLER__)
    348   end
    349 
    350   @doc """
    351   Dispatches to the path only if the request is a GET request.
    352   See `match/3` for more examples.
    353   """
    354   defmacro get(path, options, contents \\ []) do
    355     compile(:get, path, options, contents, __CALLER__)
    356   end
    357 
    358   @doc """
    359   Dispatches to the path only if the request is a HEAD request.
    360   See `match/3` for more examples.
    361   """
    362   defmacro head(path, options, contents \\ []) do
    363     compile(:head, path, options, contents, __CALLER__)
    364   end
    365 
    366   @doc """
    367   Dispatches to the path only if the request is a POST request.
    368   See `match/3` for more examples.
    369   """
    370   defmacro post(path, options, contents \\ []) do
    371     compile(:post, path, options, contents, __CALLER__)
    372   end
    373 
    374   @doc """
    375   Dispatches to the path only if the request is a PUT request.
    376   See `match/3` for more examples.
    377   """
    378   defmacro put(path, options, contents \\ []) do
    379     compile(:put, path, options, contents, __CALLER__)
    380   end
    381 
    382   @doc """
    383   Dispatches to the path only if the request is a PATCH request.
    384   See `match/3` for more examples.
    385   """
    386   defmacro patch(path, options, contents \\ []) do
    387     compile(:patch, path, options, contents, __CALLER__)
    388   end
    389 
    390   @doc """
    391   Dispatches to the path only if the request is a DELETE request.
    392   See `match/3` for more examples.
    393   """
    394   defmacro delete(path, options, contents \\ []) do
    395     compile(:delete, path, options, contents, __CALLER__)
    396   end
    397 
    398   @doc """
    399   Dispatches to the path only if the request is an OPTIONS request.
    400   See `match/3` for more examples.
    401   """
    402   defmacro options(path, options, contents \\ []) do
    403     compile(:options, path, options, contents, __CALLER__)
    404   end
    405 
    406   @doc """
    407   Forwards requests to another Plug. The `path_info` of the forwarded
    408   connection will exclude the portion of the path specified in the
    409   call to `forward`. If the path contains any parameters, those will
    410   be available in the target Plug in `conn.params` and `conn.path_params`.
    411 
    412   ## Options
    413 
    414   `forward` accepts the following options:
    415 
    416     * `:to` - a Plug the requests will be forwarded to.
    417     * `:init_opts` - the options for the target Plug. It is the preferred
    418       mechanism for passing options to the target Plug.
    419     * `:host` - a string representing the host or subdomain, exactly like in
    420       `match/3`.
    421     * `:private` - values for `conn.private`, exactly like in `match/3`.
    422     * `:assigns` - values for `conn.assigns`, exactly like in `match/3`.
    423 
    424   If `:init_opts` is undefined, then all remaining options are passed
    425   to the target plug.
    426 
    427   ## Examples
    428 
    429       forward "/users", to: UserRouter
    430 
    431   Assuming the above code, a request to `/users/sign_in` will be forwarded to
    432   the `UserRouter` plug, which will receive what it will see as a request to
    433   `/sign_in`.
    434 
    435       forward "/foo/:bar/qux", to: FooPlug
    436 
    437   Here, a request to `/foo/BAZ/qux` will be forwarded to the `FooPlug`
    438   plug, which will receive what it will see as a request to `/`,
    439   and `conn.params["bar"]` will be set to `"BAZ"`.
    440 
    441   Some other examples:
    442 
    443       forward "/foo/bar", to: :foo_bar_plug, host: "foobar."
    444       forward "/baz", to: BazPlug, init_opts: [plug_specific_option: true]
    445 
    446   """
    447   defmacro forward(path, options) do
    448     quote bind_quoted: [path: path, options: options] do
    449       {target, options} = Keyword.pop(options, :to)
    450       {options, plug_options} = Keyword.split(options, [:via, :host, :private, :assigns])
    451       plug_options = Keyword.get(plug_options, :init_opts, plug_options)
    452 
    453       if is_nil(target) or not is_atom(target) do
    454         raise ArgumentError, message: "expected :to to be an alias or an atom"
    455       end
    456 
    457       {target, target_opts} =
    458         case Atom.to_string(target) do
    459           "Elixir." <> _ -> {target, target.init(plug_options)}
    460           _ -> {{__MODULE__, target}, plug_options}
    461         end
    462 
    463       @plug_forward_target target
    464       @plug_forward_opts target_opts
    465 
    466       # Delegate the matching to the match/3 macro along with the options
    467       # specified by Keyword.split/2.
    468       match path <> "/*glob", options do
    469         Plug.forward(
    470           var!(conn),
    471           var!(glob),
    472           @plug_forward_target,
    473           @plug_forward_opts
    474         )
    475       end
    476     end
    477   end
    478 
    479   ## Match Helpers
    480 
    481   @doc false
    482   def __route__(method, path, guards, options) do
    483     {method, guards} = build_methods(List.wrap(method || options[:via]), guards)
    484     {params, match, guards, post_match} = Plug.Router.Utils.build_path_clause(path, guards)
    485     params = Plug.Router.Utils.build_path_params_match(params)
    486     private = extract_merger(options, :private)
    487     assigns = extract_merger(options, :assigns)
    488     host_match = Plug.Router.Utils.build_host_match(options[:host])
    489     {quote(do: conn), method, match, post_match, params, host_match, guards, private, assigns}
    490   end
    491 
    492   @doc false
    493   def __put_route__(conn, path, fun) do
    494     Plug.Conn.put_private(conn, :plug_route, {append_match_path(conn, path), fun})
    495   end
    496 
    497   defp append_match_path(%Plug.Conn{private: %{plug_route: {base_path, _}}}, path) do
    498     base_path <> path
    499   end
    500 
    501   defp append_match_path(%Plug.Conn{}, path) do
    502     path
    503   end
    504 
    505   # Entry point for both forward and match that is actually
    506   # responsible to compile the route.
    507   defp compile(method, expr, options, contents, caller) do
    508     {callback, options} =
    509       cond do
    510         Keyword.has_key?(contents, :do) ->
    511           {wrap_function_do(contents[:do]), expand_options(options, caller)}
    512 
    513         Keyword.has_key?(options, :do) ->
    514           {body, options} = Keyword.pop(options, :do)
    515           {wrap_function_do(body), expand_options(options, caller)}
    516 
    517         options[:to] ->
    518           options = expand_options(options, caller)
    519 
    520           callback =
    521             quote unquote: false do
    522               &(unquote(callback) / 2)
    523             end
    524 
    525           options =
    526             quote do
    527               {callback, options} = Plug.Router.__to__(unquote(caller.module), unquote(options))
    528               options
    529             end
    530 
    531           {callback, options}
    532 
    533         true ->
    534           raise ArgumentError, message: "expected one of :to or :do to be given as option"
    535       end
    536 
    537     {path, guards} = extract_path_and_guards(expr)
    538 
    539     quote bind_quoted: [
    540             method: method,
    541             path: path,
    542             options: options,
    543             guards: Macro.escape(guards, unquote: true),
    544             callback: Macro.escape(callback, unquote: true)
    545           ] do
    546       route = Plug.Router.__route__(method, path, guards, options)
    547       {conn, method, match, post_match, params, host, guards, private, assigns} = route
    548 
    549       defp do_match(unquote(conn), unquote(method), unquote(match), unquote(host))
    550            when unquote(guards) do
    551         unquote_splicing(post_match)
    552         unquote(private)
    553         unquote(assigns)
    554 
    555         params = unquote({:%{}, [], params})
    556 
    557         merge_params = fn
    558           %Plug.Conn.Unfetched{} -> params
    559           fetched -> Map.merge(fetched, params)
    560         end
    561 
    562         conn = update_in(unquote(conn).params, merge_params)
    563         conn = update_in(conn.path_params, merge_params)
    564 
    565         Plug.Router.__put_route__(conn, unquote(path), unquote(callback))
    566       end
    567     end
    568   end
    569 
    570   @doc false
    571   def __to__(module, options) do
    572     {to, options} = Keyword.pop(options, :to)
    573     {init_opts, options} = Keyword.pop(options, :init_opts, [])
    574 
    575     router_to = Module.get_attribute(module, :plug_router_to)
    576     callback = :"plug_router_to_#{map_size(router_to)}"
    577     router_to = Map.put(router_to, callback, {to, init_opts})
    578     Module.put_attribute(module, :plug_router_to, router_to)
    579     {Macro.var(callback, nil), options}
    580   end
    581 
    582   defp wrap_function_do(body) do
    583     quote do
    584       fn var!(conn), var!(opts) ->
    585         _ = var!(opts)
    586         unquote(body)
    587       end
    588     end
    589   end
    590 
    591   defp expand_options(opts, caller) do
    592     if Macro.quoted_literal?(opts) do
    593       Macro.prewalk(opts, &expand_alias(&1, caller))
    594     else
    595       opts
    596     end
    597   end
    598 
    599   defp expand_alias({:__aliases__, _, _} = alias, env),
    600     do: Macro.expand(alias, %{env | function: {:init, 1}})
    601 
    602   defp expand_alias(other, _env), do: other
    603 
    604   defp extract_merger(options, key) when is_list(options) do
    605     if option = Keyword.get(options, key) do
    606       quote do
    607         conn = update_in(conn.unquote(key), &Map.merge(&1, unquote(Macro.escape(option))))
    608       end
    609     end
    610   end
    611 
    612   # Convert the verbs given with `:via` into a variable and guard set that can
    613   # be added to the dispatch clause.
    614   defp build_methods([], guards) do
    615     {quote(do: _), guards}
    616   end
    617 
    618   defp build_methods([method], guards) do
    619     {Plug.Router.Utils.normalize_method(method), guards}
    620   end
    621 
    622   defp build_methods(methods, guards) do
    623     methods = Enum.map(methods, &Plug.Router.Utils.normalize_method(&1))
    624     var = quote do: method
    625     guards = join_guards(quote(do: unquote(var) in unquote(methods)), guards)
    626     {var, guards}
    627   end
    628 
    629   defp join_guards(fst, true), do: fst
    630   defp join_guards(fst, snd), do: quote(do: unquote(fst) and unquote(snd))
    631 
    632   # Extract the path and guards from the path.
    633   defp extract_path_and_guards({:when, _, [path, guards]}), do: {extract_path(path), guards}
    634   defp extract_path_and_guards(path), do: {extract_path(path), true}
    635 
    636   defp extract_path({:_, _, var}) when is_atom(var), do: "/*_path"
    637   defp extract_path(path), do: path
    638 end