7
votes

When we send a request we have current path info in the conn struct. Let's say for patch request.

["v1", "users", "2"] or "v1/users/2"

I am writing a plug for users authorisation based on the path info in the db. In the db the paths are stored like this:

"v1/users/:id"

This is the path we get from running mix phx.routes. Is it possible that I can get the "v1/users/:id" instead of "v1/users/2" for the current path? so I can match it with the path stored in the db.

Is there any work around?

2
It's not possible to fetch information about the matched route itself in Phoenix, since it's the job of the controller to handle it. See my answer on how you can achieve this using Regexes.Sheharyar

2 Answers

1
votes

A very hackey way to do it would be to manually replace the value in the path for the associated key.

In the conn struct, we have path_info: ["v1", "users", "2"] and path_params: %{"id" => "2"}, so we can do:

Enum.reduce(conn.path_params, conn.path_info, fn {key, value}, acc ->
  index = Enum.find_index(acc, fn x -> x == value end)
  List.replace_at(acc, index, ":#{key}")
end)
|> Enum.join("/")

And the output would be v1/users/:id.

Please be aware that this would fail if you have a parameter value that is the same as part of the route (which seems unlikely to happen). Also, if you have multiple parameters that can take the same value we would then be relying on the order of the parameters in path_params.

0
votes

Given that you have a list of user-defined routes stored in the database, you can manually check if the path of an incoming request matches one of them. Regex can help you solve this problem gracefully (although it won't be as efficient as compiled routes):

defmodule RouteMatcher do    
  def find(routes, path) do
    Enum.find_value(routes, nil, &match(&1, path))
  end

  defp match(route, path) do
    pattern = String.replace(route, ~r/:(\w+)/, ~S"(?<\g{1}>[\w-]+)")
    regex = ~r/^#{pattern}$/

    case Regex.named_captures(regex, path) do
      nil -> nil
      map -> {route, map}
    end
  end   
end

Now suppose this is the list all defined routes in the database:

routes = [
  "/v1/users",
  "/v1/users/:user_id",
  "/v1/users/:user_id/posts",
  "/v1/users/:user_id/posts/:post_id",
  "/v1/users/:user_id/posts/:post_id/:comment_id",
]

Then the RouteMatcher.find/2 function will return the first route that matches a given path along with the matched params (if no route matches it will simply return nil):

RouteMatcher.find(routes, "/v1/users")
#=> {"/v1/users", %{}}

RouteMatcher.find(routes, "/v1/users/psy")
#=> {"/v1/users/:user_id", %{"user_id" => "psy"}}

RouteMatcher.find(routes, "/v1/users/psy/posts")
#=> {"/v1/users/:user_id/posts", %{"user_id" => "psy"}}

RouteMatcher.find(routes, "/v1/users/psy/posts/hello-world")
#=> {"/v1/users/:user_id/posts/:post_id", %{"post_id" => "hello-world", "user_id" => "psy"}}

RouteMatcher.find(routes, "/v1/users/psy/posts/hello-world/45")
#=> {"/v1/users/:user_id/posts/:post_id/:comment_id", %{"comment_id" => "45", "post_id" => "hello-world", "user_id" => "psy"}}

RouteMatcher.find(routes, "/unknown/route")                    
#=> nil