Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions lib/mix/tasks/phx.gen.auth/injector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -268,17 +268,49 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
if String.contains?(code, code_to_inject) do
:already_injected
else
new_code =
code
|> String.trim_trailing()
|> String.trim_trailing("end")
|> Kernel.<>(code_to_inject)
|> Kernel.<>("end\n")

{:ok, new_code}
{:ok,
inject_before_dev_routes_scope(code, code_to_inject) ||
inject_before_final_end_fallback(code, code_to_inject)}
end
end

defp inject_before_dev_routes_scope(code, code_to_inject) do
case split_before_dev_routes(String.split(code, "\n"), []) do
{before_dev_routes, dev_routes} ->
join_injected(before_dev_routes, code_to_inject, dev_routes)

_ ->
nil
end
end

defp split_before_dev_routes([line | rest], acc) do
if String.contains?(line, "if Application.compile_env") and
String.contains?(line, ":dev_routes") do
{dev_route_comments, before_dev_routes} =
Enum.split_while(acc, &(String.trim_leading(&1) |> String.starts_with?("#")))

{Enum.reverse(before_dev_routes), Enum.reverse(dev_route_comments, [line | rest])}
else
split_before_dev_routes(rest, [line | acc])
end
end

defp split_before_dev_routes([], _acc), do: nil

defp join_injected(before_dev_routes, code_to_inject, dev_routes) do
code_to_inject = code_to_inject |> String.trim("\n") |> String.split("\n")
Enum.join(before_dev_routes ++ code_to_inject ++ [""] ++ dev_routes, "\n")
end

defp inject_before_final_end_fallback(code, code_to_inject) do
code
|> String.trim_trailing()
|> String.trim_trailing("end")
|> Kernel.<>(code_to_inject)
|> Kernel.<>("end\n")
end

@spec ensure_not_already_injected(String.t(), String.t()) :: :ok | :already_injected
defp ensure_not_already_injected(file, inject) do
if String.contains?(file, inject) do
Expand Down
215 changes: 191 additions & 24 deletions lib/phoenix/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ defmodule Phoenix.Router do
Phoenix's router is extremely efficient, as it relies on Elixir
pattern matching for matching routes and serving requests.
## Options
* `:helpers` - whether to generate route helpers. Defaults to `true`.
Helpers are deprecated, see the "Helpers" section below for more information
* `:group_by` - when set to `:verb`, Phoenix groups route clauses per HTTP
verb during compilation. This can reduce compilation times for large
routers. When enabled, all `match :*` and `forward` routes must be defined
at the end of the router
## Routing
`get/3`, `post/3`, `put/3`, and other macros named after HTTP verbs are used
Expand Down Expand Up @@ -296,9 +306,11 @@ defmodule Phoenix.Router do

defp prelude(opts) do
quote do
opts = unquote(opts)
Module.register_attribute(__MODULE__, :phoenix_routes, accumulate: true)
# TODO: Require :helpers to be explicit given
@phoenix_helpers Keyword.get(unquote(opts), :helpers, true)
@phoenix_helpers Keyword.get(opts, :helpers, true)
@phoenix_router_group_by Keyword.get(opts, :group_by, nil)

import Phoenix.Router

Expand Down Expand Up @@ -457,7 +469,7 @@ defmodule Phoenix.Router do
%{method: method, path_info: path_info, host: host} = conn = prepare(conn)
decoded = Enum.map(path_info, &URI.decode/1)

case __match_route__(decoded, method, host) do
case __match_route__(method, decoded, host) do
{metadata, prepare, pipeline, plug_opts} ->
Phoenix.Router.__call__(conn, metadata, prepare, pipeline, plug_opts)

Expand Down Expand Up @@ -495,11 +507,14 @@ defmodule Phoenix.Router do
end

{matches, {pipelines, _}} =
Enum.map_reduce(routes_with_exprs, {[], %{}}, &build_match/2)
build_matches(
routes_with_exprs,
Module.get_attribute(env.module, :phoenix_router_group_by),
env
)

routes_per_path =
routes_with_exprs
|> Enum.group_by(&elem(&1, 1).path, &elem(&1, 0))
Enum.group_by(routes_with_exprs, &elem(&1, 1).path, &elem(&1, 0))

verifies =
routes_with_exprs
Expand All @@ -515,14 +530,6 @@ defmodule Phoenix.Router do
end
end

match_catch_all =
quote generated: true do
@doc false
def __match_route__(_path_info, _verb, _host) do
:error
end
end

forward_catch_all =
quote generated: true do
@doc false
Expand Down Expand Up @@ -560,7 +567,6 @@ defmodule Phoenix.Router do
unquote(verifies)
unquote(verify_catch_all)
unquote(matches)
unquote(match_catch_all)
unquote(forward_catch_all)
end
end
Expand Down Expand Up @@ -591,22 +597,172 @@ defmodule Phoenix.Router do
end
end

defp build_matches(routes_exprs, nil, _env) do
{matches, acc} = Enum.map_reduce(routes_exprs, {[], %{}}, &build_match/2)

match_catch_all =
quote generated: true do
@doc false
def __match_route__(_verb, _path, _host) do
:error
end
end

{[matches, match_catch_all], acc}
end

defp build_matches(routes_exprs, :verb, env) do
validate_group_by_verb!(routes_exprs, env)

routes_exprs
|> Enum.group_by(&elem(&1, 0).verb)
|> Map.pop(:*, [])
|> then(fn {match_routes_exprs, map} ->
Map.to_list(map) ++ [{:*, match_routes_exprs}]
end)
|> Enum.map_reduce({[], %{}}, &build_match_verb/2)
end

defp build_matches(_routes_exprs, group_by, _env) do
raise ArgumentError,
"expected :group_by to be :verb or nil, got: #{inspect(group_by)}"
end

defp validate_group_by_verb!(routes_exprs, env) do
routes = Enum.map(routes_exprs, &elem(&1, 0))

case routes_after_catch_all(routes, nil, []) do
[] ->
:ok

violations ->
[{route, _catch_all} | _] = violations

raise CompileError,
file: env.file,
line: route.line,
description: group_by_verb_error(violations)
end
end

defp routes_after_catch_all([route | routes], catch_all, acc) do
cond do
catch_all && !catch_all_route?(route) ->
routes_after_catch_all(routes, catch_all, [{route, catch_all} | acc])

catch_all_route?(route) ->
routes_after_catch_all(routes, catch_all || route, acc)

true ->
routes_after_catch_all(routes, catch_all, acc)
end
end

defp routes_after_catch_all([], _catch_all, acc), do: Enum.reverse(acc)

defp catch_all_route?(%{kind: :forward}), do: true
defp catch_all_route?(%{verb: :*}), do: true
defp catch_all_route?(_route), do: false

defp catch_all_description(%{kind: :forward, path: path}), do: "forward #{inspect(path)}"
defp catch_all_description(%{path: path}), do: "match :*, #{inspect(path)}"

defp group_by_verb_error(violations) do
routes =
Enum.map_join(violations, "\n", fn {route, catch_all} ->
" * #{inspect(route.path)} after #{catch_all_description(catch_all)}"
end)

"""
cannot compile router with group_by: :verb because routes were found after a match :* or forward.
Define all match :* and forward routes at the end of the router.
#{routes}
"""
end

defp build_match_verb({:*, routes_exprs}, acc) do
name = :__match_route_catch_all__

{clauses, acc} =
Enum.map_reduce(routes_exprs, acc, &build_match_path(:defp, name, [], &1, &2))

dispatch =
quote generated: true do
unquote({:__block__, [], clauses})

defp __match_route_catch_all__(_path, _host) do
:error
end

@doc false
def __match_route__(_, path, host) do
__match_route_catch_all__(path, host)
end
end

{dispatch, acc}
end

defp build_match_verb({verb, routes_exprs}, acc) do
pattern = verb |> to_string() |> String.upcase()
name = :"__match_route_#{verb}__"

{clauses, acc} =
Enum.map_reduce(routes_exprs, acc, &build_match_path(:defp, name, [], &1, &2))

dispatch =
quote generated: true do
unquote({:__block__, [], clauses})

defp unquote(name)(path, host) do
__match_route_catch_all__(path, host)
end

def __match_route__(unquote(pattern), path, host) do
unquote(name)(path, host)
end
end

{dispatch, acc}
end

defp build_match({route, expr}, {acc_pipes, known_pipes}) do
verb_match =
case route.verb do
:* -> Macro.var(:_verb, nil)
verb -> verb |> to_string() |> String.upcase()
end

{clauses, acc} =
build_match_path(
:def,
:__match_route__,
[verb_match],
{route, expr},
{acc_pipes, known_pipes}
)

{clauses, acc}
end

defp build_match_path(kind, name, prefix, {route, expr}, {acc_pipes, known_pipes}) do
{pipe_name, acc_pipes, known_pipes} = build_match_pipes(route, acc_pipes, known_pipes)

%{
prepare: prepare,
dispatch: dispatch,
verb_match: verb_match,
path_params: path_params,
hosts: hosts,
path: path
} = expr

clauses =
for host <- hosts do
args = prefix ++ [path, host]

quote line: route.line do
def __match_route__(unquote(path), unquote(verb_match), unquote(host)) do
unquote(kind)(unquote(name)(unquote_splicing(args))) do
{unquote(build_metadata(route, path_params)),
fn var!(conn, :conn), %{path_params: var!(path_params, :conn)} ->
unquote(prepare)
Expand Down Expand Up @@ -676,28 +832,36 @@ defmodule Phoenix.Router do
Useful for defining routes not included in the built-in macros.
The catch-all verb, `:*`, may also be used to match all HTTP methods.
If the router is configured with `group_by: :verb`, all `match :*` routes
must be defined at the end of the router, after all routes with explicit verbs.
## Options
* `:as` - configures the named helper. If `nil`, does not generate
a helper. Has no effect when using verified routes exclusively
* `:alias` - configure if the scope alias should be applied to the route.
Defaults to true, disables scoping if false.
Defaults to true, disables scoping if false
* `:log` - the level to log the route dispatching under, may be set to false. Defaults to
`:debug`. Route dispatching contains information about how the route is handled (which controller
action is called, what parameters are available and which pipelines are used) and is separate from
the plug level logging. To alter the plug log level, please see
https://phoenix.hexdocs.pm/Phoenix.Logger.html#module-dynamic-log-level.
https://phoenix.hexdocs.pm/Phoenix.Logger.html#module-dynamic-log-level
* `:private` - a map of private data to merge into the connection
when a route matches
* `:assigns` - a map of data to merge into the connection when a route matches
* `:metadata` - a map of metadata used by the telemetry events and returned by
`route_info/4`. The `:mfa` field is used by telemetry to print logs and by the
router to emit compile time checks. Custom fields may be added.
* `:warn_on_verify` - the boolean for whether matches to this route trigger
an unmatched route warning for `Phoenix.VerifiedRoutes`. It is useful to ignore
an otherwise catch-all route definition from being matched when verifying routes.
Defaults `false`.
router to emit compile time checks. Custom fields may be added
* `:warn_on_verify` - the boolean for whether matches to this route in verified
routes should emit a warning, rather than being accepted as verified. It is useful
to ignore an otherwise catch-all route definition from being matched when verifying
routes. Defaults `false`
## Examples
Expand Down Expand Up @@ -1203,6 +1367,9 @@ defmodule Phoenix.Router do
The router pipelines will be invoked prior to forwarding the
connection.
If the router is configured with `group_by: :verb`, all forwards must be
defined at the end of the router, after all routes with explicit verbs.
## Examples
scope "/", MyApp do
Expand Down Expand Up @@ -1266,7 +1433,7 @@ defmodule Phoenix.Router do

def route_info(router, method, split_path, host) when is_list(split_path) do
with {metadata, _prepare, _pipeline, {_plug, _opts}} <-
router.__match_route__(split_path, method, host) do
router.__match_route__(method, split_path, host) do
Map.delete(metadata, :conn)
end
end
Expand Down
6 changes: 1 addition & 5 deletions lib/phoenix/router/route.ex
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,7 @@ defmodule Phoenix.Router.Route do
dispatch: build_dispatch(route),
hosts: build_host_match(route.hosts),
path_params: build_path_params(binding),
prepare: build_prepare(route),
verb_match: verb_match(route.verb)
prepare: build_prepare(route)
}
end

Expand All @@ -138,9 +137,6 @@ defmodule Phoenix.Router.Route do
for host <- hosts, do: Plug.Router.Utils.build_host_match(host)
end

defp verb_match(:*), do: Macro.var(:_verb, nil)
defp verb_match(verb), do: verb |> to_string() |> String.upcase()

defp build_path_params(binding), do: {:%{}, [], binding}

defp build_path_and_binding(%Route{path: path} = route) do
Expand Down
Loading
Loading