Repository: phoenixframework/phoenix (elixir)

Issue:
Show socket paths on `mix phx.routes`
Currently only the paths defined in the router are displayed. It'd be nice to also display the paths for sockets and which modules the sockets route to.

Pull request description:
Show socket paths on mix phx.routes. Closes #3237
@Gazler let me know your thoughts on the formatting in relation to the router output. Thanks!

Patch:
diff --git a/lib/mix/tasks/phx.routes.ex b/lib/mix/tasks/phx.routes.ex
index 96b8ee7fc..7bc8f79f1 100644
--- a/lib/mix/tasks/phx.routes.ex
+++ b/lib/mix/tasks/phx.routes.ex
@@ -27,15 +27,27 @@ defmodule Mix.Tasks.Phx.Routes do
 
   @doc false
   def run(args, base \\ Mix.Phoenix.base()) do
-    Mix.Task.run "compile", args
+    Mix.Task.run("compile", args)
 
-    args
-    |> Enum.at(0)
-    |> router(base)
-    |> ConsoleFormatter.format()
+    {router_mod, opts} =
+      case OptionParser.parse(args, switches: [enpdoint: :string, router: :string]) do
+        {opts, [passed_router], _} -> {router(passed_router, base), opts}
+        {opts, [], _} -> {router(opts[:router], base), opts}
+      end
+
+
+    router_mod
+    |> ConsoleFormatter.format(endpoint(opts[:endpoint], base))
     |> Mix.shell.info()
   end
 
+  defp endpoint(nil, base) do
+    loaded(web_mod(base, "Endpoint"))
+  end
+  defp endpoint(module, base) do
+    loaded(Module.concat([module]))
+  end
+
   defp router(nil, base) do
     if Mix.Project.umbrella?() do
       Mix.raise """
@@ -55,11 +67,13 @@ defmodule Mix.Tasks.Phx.Routes do
     """
   end
   defp router(router_name, _base) do
-    arg_router = Module.concat("Elixir", router_name)
-    loaded(arg_router) || Mix.raise "the provided router, #{inspect arg_router}, does not exist"
+    arg_router = Module.concat([router_name])
+    loaded(arg_router) || Mix.raise "the provided router, #{inspect(arg_router)}, does not exist"
   end
 
-  defp loaded(module), do: Code.ensure_loaded?(module) && module
+  defp loaded(module) do
+    if Code.ensure_loaded?(module), do: module
+  end
 
   defp app_mod(base, name), do: Module.concat([base, name])
 
diff --git a/lib/phoenix/router/console_formatter.ex b/lib/phoenix/router/console_formatter.ex
index 9f9539413..fb68dd6f8 100644
--- a/lib/phoenix/router/console_formatter.ex
+++ b/lib/phoenix/router/console_formatter.ex
@@ -5,25 +5,83 @@ defmodule Phoenix.Router.ConsoleFormatter do
   @doc """
   Format the routes for printing.
   """
-  def format(router) do
-    routes = router.__routes__
-    column_widths = calculate_column_widths(routes)
-    Enum.map_join(routes, "", &format_route(&1, column_widths))
+  def format(router, endpoint \\ nil) do
+    routes = router.__routes__()
+    column_widths = calculate_column_widths(routes, endpoint)
+
+    routes
+    |> Enum.map_join("", &format_route(&1, column_widths))
+    |> Kernel.<>(format_endpoint(endpoint, column_widths))
   end
 
-  defp calculate_column_widths(routes) do
-    Enum.reduce routes, {0, 0, 0}, fn(route, acc) ->
-      %Route{verb: verb, path: path, helper: helper} = route
-      verb = verb_name(verb)
-      {verb_len, path_len, route_name_len} = acc
-      route_name = route_name(helper)
+  defp format_endpoint(nil, _), do: ""
+  defp format_endpoint(endpoint, widths) do
+    case endpoint.__sockets__() do
+      [] -> ""
+      sockets ->
+        Enum.map_join(sockets, "", fn socket ->
+          format_websocket(socket, widths) <>
+          format_longpoll(socket, widths)
+        end)
+      end
+  end
+
+  defp format_websocket({_path, Phoenix.LiveReloader.Socket, _opts}, _), do: ""
+  defp format_websocket({path, module, opts}, widths) do
+    if opts[:websocket] != false do
+      {verb_len, path_len, route_name_len} = widths
+
+      String.pad_leading("websocket", route_name_len) <> "  " <>
+      String.pad_trailing("WS", verb_len) <> "  " <>
+      String.pad_trailing(path <> "/websocket", path_len) <> "  " <>
+      inspect(module) <>
+      "\n"
+    else
+      ""
+    end
+  end
+
+  defp format_longpoll({_path, Phoenix.LiveReloader.Socket, _opts}, _), do: ""
+  defp format_longpoll({path, module, opts}, widths) do
+    if opts[:longpoll] != false do
+      for method <- ["GET", "POST"], into: "" do
+        {verb_len, path_len, route_name_len} = widths
 
-      {max(verb_len, String.length(verb)),
-       max(path_len, String.length(path)),
-       max(route_name_len, String.length(route_name))}
+        String.pad_leading("longpoll", route_name_len) <> "  " <>
+        String.pad_trailing(method, verb_len) <> "  " <>
+        String.pad_trailing(path <> "/longpoll", path_len) <> "  " <>
+        inspect(module) <>
+        "\n"
+      end
+    else
+      ""
     end
   end
 
+  defp calculate_column_widths(routes, endpoint) do
+    sockets = endpoint && endpoint.__sockets__() || []
+
+    widths =
+      Enum.reduce(routes, {0, 0, 0}, fn route, acc ->
+        %Route{verb: verb, path: path, helper: helper} = route
+        verb = verb_name(verb)
+        {verb_len, path_len, route_name_len} = acc
+        route_name = route_name(helper)
+
+        {max(verb_len, String.length(verb)),
+        max(path_len, String.length(path)),
+        max(route_name_len, String.length(route_name))}
+      end)
+
+    Enum.reduce(sockets, widths, fn {path, _mod, _opts}, acc ->
+      {verb_len, path_len, route_name_len} = acc
+
+      {verb_len,
+       max(path_len, String.length(path <> "/websocket")),
+       max(route_name_len, String.length("websocket"))}
+    end)
+  end
+
   defp format_route(route, column_widths) do
     %Route{verb: verb, path: path, plug: plug,
            opts: opts, helper: helper} = route


Test patch:
diff --git a/test/phoenix/router/console_formatter_test.exs b/test/phoenix/router/console_formatter_test.exs
index 919f0cc0e..15fe6170e 100644
--- a/test/phoenix/router/console_formatter_test.exs
+++ b/test/phoenix/router/console_formatter_test.exs
@@ -57,7 +57,38 @@ defmodule Phoenix.Router.ConsoleFormatterTest do
     """
   end
 
-  defp draw(router) do
-    ConsoleFormatter.format(router)
+  describe "endpoint sockets" do
+
+    def __sockets__, do: []
+
+    defmodule FormatterEndpoint do
+      use Phoenix.Endpoint, otp_app: :phoenix
+
+      socket "/socket", TestSocket, websocket: true
+    end
+
+    test "format with sockets" do
+      assert draw(RouterTestSingleRoutes, FormatterEndpoint) == """
+              page_path  GET     /                  Phoenix.PageController :index
+      upload_image_path  POST    /images            Phoenix.ImageController :upload
+      remove_image_path  DELETE  /images            Phoenix.ImageController :delete
+              websocket  WS      /socket/websocket  TestSocket
+               longpoll  GET     /socket/longpoll   TestSocket
+               longpoll  POST    /socket/longpoll   TestSocket
+      """
+    end
+
+
+    test "format without sockets" do
+      assert draw(RouterTestSingleRoutes, __MODULE__) == """
+              page_path  GET     /        Phoenix.PageController :index
+      upload_image_path  POST    /images  Phoenix.ImageController :upload
+      remove_image_path  DELETE  /images  Phoenix.ImageController :delete
+      """
+    end
+  end
+
+  defp draw(router, endpoint \\ nil) do
+    ConsoleFormatter.format(router, endpoint)
   end
 end