Metaprogramming in Phoenix router

Recently, when reading Phoenix routing guide I stumbled upon description of resources:

scope "/", HelloPhoenix do
  pipe_through :browser

  resources "/users", UserController
end

And having Rails background I thought that it would be nice to get the framework guess the controller name based on path.

  resources "/users"

Then I added matching function to deps/phoenix/lib/phoenix/router.ex

defmacro resources(path) do
  controller_name = path                             # "/users"
    |> String.replace_prefix("/", "")                # "users"
    |> String.split("_")                             # ["users"]
    |> Enum.map(fn(el) -> String.capitalize(el) end) # ["Users"]
    |> Enum.join()                                   # "Users"
  # create a fiction result of quote function
  controller = {:__aliases__, [], [String.to_atom(controller_name <> "Controller")]}
  add_resources path, controller, [], do: nil
end

After phoenix recompilation with mix deps.compile phoenix new router function magically started to work.

# web/router.ex
scope "/", HelloPhoenix do
  pipe_through :browser # Use the default browser stack

  resources "/users"
end

scope "/admin", HelloPhoenix.Admin, as: :admin do
  pipe_through :browser

  resources "/users"
end

$mix phoenix.routes

      users_path  GET     /users                 HelloPhoenix.UsersController :index
      users_path  GET     /users/:id/edit        HelloPhoenix.UsersController :edit
      users_path  GET     /users/new             HelloPhoenix.UsersController :new
      users_path  GET     /users/:id             HelloPhoenix.UsersController :show
      users_path  POST    /users                 HelloPhoenix.UsersController :create
      users_path  PATCH   /users/:id             HelloPhoenix.UsersController :update
                  PUT     /users/:id             HelloPhoenix.UsersController :update
      users_path  DELETE  /users/:id             HelloPhoenix.UsersController :delete
admin_users_path  GET     /admin/users           HelloPhoenix.Admin.UsersController :index
admin_users_path  GET     /admin/users/:id/edit  HelloPhoenix.Admin.UsersController :edit
admin_users_path  GET     /admin/users/new       HelloPhoenix.Admin.UsersController :new
admin_users_path  GET     /admin/users/:id       HelloPhoenix.Admin.UsersController :show
admin_users_path  POST    /admin/users           HelloPhoenix.Admin.UsersController :create
admin_users_path  PATCH   /admin/users/:id       HelloPhoenix.Admin.UsersController :update
                  PUT     /admin/users/:id       HelloPhoenix.Admin.UsersController :update
admin_users_path  DELETE  /admin/users/:id       HelloPhoenix.Admin.UsersController :delete

So the question is about ugly transformation that tries to mimic #constantize method in Rails. Is there a better way to guess the constant based on the given string?

1 Like

The Phoenix.Naming module has something like this.

path
|> String.replace_prefix("/", "")
|> String.split("/")
|> Enum.map(&Phoenix.Naming.camelize/1)
|> Enum.join(".")

Or take a look at Macro.camelize/1 if you can guarantee your routes can be cleanly turned into module names.

2 Likes

In addition, if you really want this feature (to each his own), you don’t have to edit your Phoenix dep to get it.

As long as you’re willing to come up with a new name and not use resources, you can create your own macro in your router that takes a path and translates it into resources "/users", UsersController.

Something like:

defmacro clever_resource(path)
  controller = .... # like your code above
  quote do
    resources unquote(path), unquote(controller)
  end
end

Then in your router you call it with:

clever_resources "/users"
1 Like