Last active
January 7, 2025 07:39
-
-
Save andreaseriksson/e454b9244a734310d4ab74d8595f98cd to your computer and use it in GitHub Desktop.
This is a mix task for converting old Phoenix routes to new verified routes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Mix.Tasks.ConvertToVerifiedRoutes do | |
@shortdoc "Fix routes" | |
use Mix.Task | |
@regex ~r/(Routes\.)(.*)_(path|url)\(.*?\)/ | |
@web_module MyAppWeb | |
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) | |
:ok | |
end | |
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 | |
def ask_about_replacement(content, route, learnings) do | |
route = String.trim(route) | |
if verified_route = find_verified_route_from_string(route) do | |
replacement = Map.get(learnings, route) || ask_for_direct_match(route, verified_route) || ask_for_fallback(route, verified_route) | |
if replacement && (String.starts_with?(replacement, "~p\"/") || String.starts_with?(replacement, "url")) do | |
replace_content( | |
String.replace(content, route, replacement), | |
Map.put(learnings, route, replacement) | |
) | |
end | |
else | |
{:ok, content, learnings} | |
end | |
end | |
def ask_for_fallback(route, _verified_route) do | |
response = Mix.shell().prompt( | |
""" | |
What is the verified route for (type "skip" for skipping): | |
#{IO.ANSI.red}#{route}#{IO.ANSI.reset} | |
Start with: | |
~p"/.. | |
""") | |
response = String.trim("#{response}") | |
response != "" && response | |
end | |
def ask_for_direct_match(route, verified_route) do | |
if Mix.shell().yes?( | |
""" | |
Should we replace | |
#{IO.ANSI.red}#{route}#{IO.ANSI.reset} | |
with | |
#{IO.ANSI.green}#{verified_route}#{IO.ANSI.reset} | |
""") do | |
verified_route | |
end | |
end | |
def find_verified_route_from_string(route) do | |
parts = | |
route | |
|> String.replace("Routes.", "") | |
|> String.split("(") | |
with [route_helper, arguments|_] <- parts do | |
arguments = | |
arguments | |
|> String.replace_trailing(")", "") | |
|> String.split(",") | |
|> Enum.map(&String.trim/1) | |
@web_module.Router.__routes__() | |
|> Enum.find(fn %{helper: helper, plug_opts: plug_opts} -> | |
is_atom(plug_opts) && | |
Enum.member?(arguments, ":#{plug_opts}") && | |
String.starts_with?(route_helper, helper) | |
end) | |
|> case do | |
%{path: "" <> path} -> | |
path = interpolate_path_with_vars(path, arguments) | |
path = maybe_add_params(path, arguments) | |
if String.contains?(route_helper, "_url") do | |
~s[url(~p"#{path}")] | |
else | |
~s(~p"#{path}") | |
end | |
_ -> | |
nil | |
end | |
end | |
end | |
def interpolate_path_with_vars(path, arguments) do | |
arguments = Enum.slice(arguments, 2..10) | |
path | |
|> String.split("/") | |
|> Enum.filter(&String.starts_with?(&1, ":")) | |
|> Enum.with_index() | |
|> Enum.reduce(path, fn {slot, idx}, memo -> | |
argument = Enum.at(arguments, idx) | |
argument = "{#{argument}}" | |
String.replace(memo, slot, "##{argument}", global: false) | |
end) | |
end | |
def maybe_add_params(path, arguments) do | |
case Enum.filter(arguments, &String.contains?(&1, ": ")) do | |
[] -> | |
if "params" in arguments do | |
query_params = "{params}" | |
"#{path}?##{query_params}" | |
else | |
path | |
end | |
query_params -> | |
query_params = "{#{inspect(query_params) |> String.replace("\"", "") }}" | |
"#{path}?##{query_params}" | |
end | |
end | |
end |
Added some simple support for params
Thanks for the tool! I wrote up my experience using this and how to clean-up some of the left-over bits. https://fly.io/phoenix-files/migrating-to-verified-routes/
I followed the fly article and found that the task also seems to have trouble with nested routes (e.g. Routes.user_comments_path(conn, @user)
would be changed to ~p"/users/#{@user}"
, leaving off the trailing /comments
).
* (UndefinedFunctionError) function AdminWeb.Router.__routes__/0 is undefined (module AdminWeb.Router is not available)
AdminWeb.Router.__routes__()
(kc_core 0.1.0) lib/mix/tasks/convert_to_verified_routes.ex:96: Mix.Tasks.ConvertToVerifiedRoutes.find_verified_route_from_string/1
(kc_core 0.1.0) lib/mix/tasks/convert_to_verified_routes.ex:45: Mix.Tasks.ConvertToVerifiedRoutes.ask_about_replacement/3
(kc_core 0.1.0) lib/mix/tasks/convert_to_verified_routes.ex:26: Mix.Tasks.ConvertToVerifiedRoutes.test_filename/2
(elixir 1.14.1) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(kc_core 0.1.0) lib/mix/tasks/convert_to_verified_routes.ex:14: Mix.Tasks.ConvertToVerifiedRoutes.run/1
(mix 1.14.1) lib/mix/task.ex:421: anonymous fn/3 in Mix.Task.run_task/4
(mix 1.14.1) lib/mix/cli.ex:84: Mix.CLI.run_task/2
Running inside umbrella app for a particular version.
Phoenix.Router.routes( @web_module.Router)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Run this with