Repository: phoenixframework/phoenix
Pull request #3419 (2019-05-17 18:57:54)

Add route_info lookup
@josevalim any input on how we should partially make `%Phoenix.Router.Route{}` public? Still needs docs about what is and isn't private, but not sure what's best at the moduledoc/or router_info function doc level. Would appreciate your thoughts 

josevalim: I think all of the current fields are safe to expose, so it is probably a matter of making the module public and hiding the documentation for all other functions (or perhaps move those functions to another module, so we don't end-up `@doc false` everything).

Patch:
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index 599acea1b..4cfb53f26 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -845,4 +845,39 @@ defmodule Phoenix.Router do
       unquote(add_route(:forward, :*, path, plug, plug_opts, router_opts))
     end
   end
+
+  @doc """
+  Returns the compile-time route info and runtime path params for a request.
+
+  A map of metadata is returned with the following keys:
+
+    * `:log` - the configured log level. For example `:debug`
+    * `:path_params` - the map of runtime path params
+    * `:pipe_through` - the list of pipelines for the route's scope, for example `[:browser]`
+    * `:plug` - the plug to dipatch the route to, for example `AppWeb.PostController`
+    * `:plug_opts` - the options to pass when calling the plug, for example: `:index`
+    * `:route` - the string route pattern, such as `"/posts/:id"`
+
+  ## Examples
+
+      iex> Phoenix.Router.route_info(AppWeb.Router, "GET", "/posts/123", "myhost")
+      %{
+        log: :debug,
+        path_params: %{"id" => "123"},
+        pipe_through: [:browser],
+        plug: AppWeb.PostController,
+        plug_opts: :show,
+        route: "/posts/:id",
+      }
+
+      iex> Phoenix.Router.route_info(MyRouter, "GET", "/not-exists", "myhost")
+      :error
+  """
+  def route_info(router, method, path, host) do
+    split_path = for segment <- String.split(path, "/"), segment != "", do: segment
+    case router.__match_route__(method, split_path, host) do
+      {%{} = metadata, _prepare, _pipeline, {_plug, _opts}} -> Map.delete(metadata, :conn)
+      :error -> :error
+    end
+  end
 end


Test patch:
diff --git a/lib/phoenix/test/conn_test.ex b/lib/phoenix/test/conn_test.ex
index 1ad99ea70..26afa2857 100644
--- a/lib/phoenix/test/conn_test.ex
+++ b/lib/phoenix/test/conn_test.ex
@@ -575,18 +575,14 @@ defmodule Phoenix.ConnTest do
   def redirected_params(%Plug.Conn{} = conn) do
     router = Phoenix.Controller.router_module(conn)
     %URI{path: path, host: host} = conn |> redirected_to() |> URI.parse()
-    path_info = split_path(path)
 
-    case router.__match_route__("GET", path_info, host || conn.host) do
+    case Phoenix.Router.route_info(router, "GET", path, host || conn.host) do
       :error ->
         raise Phoenix.Router.NoRouteError, conn: conn, router: router
-      {%{path_params: path_params}, _prepare, _pipes, _dispatch} ->
+      %{path_params: path_params} ->
         Enum.into(path_params, %{}, fn {key, val} -> {String.to_atom(key), val} end)
     end
   end
-  defp split_path(path) do
-    for segment <- String.split(path, "/"), segment != "", do: segment
-  end
 
   @doc """
   Asserts an error was wrapped and sent with the given status.
diff --git a/test/phoenix/router/routing_test.exs b/test/phoenix/router/routing_test.exs
index 2bc08aa9a..22240eb7a 100644
--- a/test/phoenix/router/routing_test.exs
+++ b/test/phoenix/router/routing_test.exs
@@ -36,7 +36,7 @@ defmodule Phoenix.Router.RoutingTest do
     get "/backups/*path", UserController, :image
     get "/static/images/icons/*image", UserController, :image
 
-    trace "/trace", UserController, :trace
+    trace("/trace", UserController, :trace)
     options "/options", UserController, :options
     connect "/connect", UserController, :connect
     match :move, "/move", UserController, :move
@@ -189,26 +189,26 @@ defmodule Phoenix.Router.RoutingTest do
 
     test "logs controller and action with (path) parameters" do
       assert capture_log(fn -> call(Router, :get, "/users/1", foo: "bar") end) =~ """
-      [debug] Processing with Phoenix.Router.RoutingTest.UserController.show/2
-        Parameters: %{"foo" => "bar", "id" => "1"}
-        Pipelines: []
-      """
+             [debug] Processing with Phoenix.Router.RoutingTest.UserController.show/2
+               Parameters: %{"foo" => "bar", "id" => "1"}
+               Pipelines: []
+             """
     end
 
     test "logs controller and action with filtered parameters" do
       assert capture_log(fn -> call(Router, :get, "/users/1", password: "bar") end) =~ """
-      [debug] Processing with Phoenix.Router.RoutingTest.UserController.show/2
-        Parameters: %{"id" => "1", "password" => "[FILTERED]"}
-        Pipelines: []
-      """
+             [debug] Processing with Phoenix.Router.RoutingTest.UserController.show/2
+               Parameters: %{"id" => "1", "password" => "[FILTERED]"}
+               Pipelines: []
+             """
     end
 
     test "logs plug with pipeline and custom level" do
       assert capture_log(fn -> call(Router, :get, "/plug") end) =~ """
-      [info]  Processing with Phoenix.Router.RoutingTest.SomePlug
-        Parameters: %{}
-        Pipelines: [:noop]
-      """
+             [info]  Processing with Phoenix.Router.RoutingTest.SomePlug
+               Parameters: %{}
+               Pipelines: [:noop]
+             """
     end
 
     test "does not log when log is set to false" do
@@ -216,4 +216,35 @@ defmodule Phoenix.Router.RoutingTest do
                "Processing with Phoenix.Router.RoutingTest.SomePlug"
     end
   end
+
+  test "route_info returns route string and path params" do
+    assert Phoenix.Router.route_info(Router, "GET", "foo/bar/baz", nil) == %{
+             log: :debug,
+             path_params: %{"path" => ["foo", "bar", "baz"]},
+             pipe_through: [],
+             plug: Phoenix.Router.RoutingTest.UserController,
+             plug_opts: :not_found,
+             route: "/*path"
+           }
+
+    assert Phoenix.Router.route_info(Router, "GET", "users/1", nil) == %{
+             log: :debug,
+             path_params: %{"id" => "1"},
+             pipe_through: [],
+             plug: Phoenix.Router.RoutingTest.UserController,
+             plug_opts: :show,
+             route: "/users/:id",
+           }
+
+    assert Phoenix.Router.route_info(Router, "GET", "/", "host") == %{
+             log: :debug,
+             path_params: %{},
+             pipe_through: [],
+             plug: Phoenix.Router.RoutingTest.UserController,
+             plug_opts: :index,
+             route: "/",
+           }
+
+    assert Phoenix.Router.route_info(Router, "POST", "/not-exists", "host") == :error
+  end
 end
diff --git a/test/phoenix/test/conn_test.exs b/test/phoenix/test/conn_test.exs
index 1ab22cfa8..d5fa67f10 100644
--- a/test/phoenix/test/conn_test.exs
+++ b/test/phoenix/test/conn_test.exs
@@ -40,6 +40,8 @@ defmodule Phoenix.Test.ConnTest do
   use Phoenix.ConnTest
   alias Phoenix.Test.ConnTest.{Router, RedirRouter}
 
+  @moduletag :capture_log
+
   defmodule ConnError do
     defexception [message: nil, plug_status: 500]
   end
@@ -188,7 +190,7 @@ defmodule Phoenix.Test.ConnTest do
         port: 111317,
         ssl_cert: <<1, 2, 3, 4>>
       }
-      conn = 
+      conn =
         build_conn()
         |> Plug.Test.put_peer_data(peer_data)