Tutorial

Automatic convertion to verified routes

Phoenix 1.7

One of the nicest features in the new Phoenix 1.7 is the verified routes. Instead of the previous auto generated route-function, there is a p-sigil that looks like the browser path that makes it way easier to work with.

However, if you work on a large Phoenix application with a lot of files and routes, going through and switching to verified routes can be a real pain. So, in this tutorial, I will go though a MIX-task that I made that will automatically go through all the files and propt the developer with a suggestion for the verified route.

In this tutorial I will go through how I made the mix task that allowed by to go through and convert all ols routes to the new verified routes.

Step 1 - Create an empty mix task

When creating this mix task, I will create to module name spaced under Mix Tasks and then the name of the module. The name of the module will decide on the command that you need to enter in the console.

Since this is a one-off that I don’t intend to include in the project, I will just use the basic namespace.

defmodule Mix.Tasks.ConvertToVerifiedRoutes do
  @shortdoc "Convert routes to verified routes"
  use Mix.Task

  def run(_) do

    :ok
  end
end

If I test this in the console, with the command mix convert_to_verified_routes it should juist return the :ok atom.

Step 2 - Gather a list of all files with routes

Next part is to gather all files in the lib-filder and the test-folder and filter out those files that contains the string Routes.. With that list, I wan’t to go through each file and recursively convert each instance of the old routes to the new verified routes.

In that process, I wan’t to build up the replacement rules in a map, so I don’t need to answer the same question for a route-string that exists in more than one file. That map is called learnings below.

def run(_) do
  Path.wildcard("lib/**/*.*ex")
  |> Enum.concat(Path.wildcard("test/**/*.*ex*"))
  |> Enum.sort()
  |> Enum.filter(&(File.read!(&1) |> String.contains?("Routes.")))
  |> Enum.reduce(%{}, fn filename, learnings ->
    test_filename(filename, learnings)
  end)
end

Step 3 - Read file content and find route with regex

I am using regex to find the routes path. This assumes routes that have the shape with parenthesis like below:

@regex ~r/(Routes\\.)(.*)_(path|url)\\(.*?\\)/
# Routes.admin_path(@conn, :index)

Next part is the function test_filename. Note that I pass in the filename and the learnings map mentioned above.

In starting with reading the file content to a string and pass that into the replace_content function. If the function returns the ok-tuple, I am done and writes the content to the file.

Also note that I return learnings to that it can be passed on to the next file.

def test_filename(filename, learnings) do
  Mix.shell().info(filename)

  content = File.read!(filename)

  case replace_content(content, learnings) do
    {:ok, content, learnings} ->
      File.write!(filename, content)
      learnings
    _ ->
      learnings
  end
end

def replace_content(content, learnings) do
  case Regex.run(@regex, content) do
    [route|_] -> ask_about_replacement(content, route, learnings)
    _ -> {:ok, content, learnings}
  end
end

The second part here is the replace_content function. This uses the regex from above to identify the first instance of an old route. If it finds a match, I then call the function ask_about_replacement.

If it does not find a match, it means that all occurrences has been found and we are done with this file and can write it to the file.

Step 4 - Collect information about current routes

Before I can start asking about if the verified route is correct, I first need to identify it. It turns out that there is a function for generating the routes to a list.

MyAppWeb.Router.__routes__()

That returns a list of maps with the shape:

%{
  helper: "user_registration",
  metadata: %{log: :debug},
  path: "/sign_up",
  plug: MyAppWeb.UserRegistrationController,
  plug_opts: :new,
  verb: :get
},

What I need to identify the verified route is the :helper and :verb. And I will use the :path to generate the string ~p"/sign_up"

That logic is inside the function:

def find_verified_route_from_string(route) do
  # ..
end

Step 5 - Confirm each change

Since I don’t trust myself, I wan’t to confirm manually on each new occasion of a route. For that, I am using a function built into Mix. It is Mix.shell().yes?/1 that presents the user with a yes or no question.

However, the relevant logic is like this:

replacement = Map.get(learnings, route) || ask_for_direct_match(route, verified_route) || ask_for_fallback(route, verified_route)

First look in the learnings, to see if I have already solved it. Otherwise I will ask for a direct match. If I answer no on the direct match-question, I have the option to manually write the verified route.

Conclusion

This seems to work great for me. But note, I haven’t implemented logic for query params. So this is something you can use the manual input for. I dont use that many routes with query params so I will just handle that manually.