A smarter Rails url_for helper
Problem
In my Rails application, I want to be able to determine a URL for any given model, without the calling code having to know the type of the model. Instead of calling post_path(@post)
or comment_path(@comment)
, I want to just say url_for(@post)
or url_for(@comment)
. This already works in many cases, when Rails can infer the correct route helper method to call. However, Rails cannot always infer the correct helper, and even if it does, it might not be the one you want.
For example, suppose you have comments as a nested resource under posts (config/routes.rb):
resources :posts do
resources :comments
end
I have a @post
with id 8, which has a @comment
with id 4. If I call url_for(@post)
, it will correctly resolve to /posts/8
. However, if I call url_for(@comment)
, I get an exception:
undefined method `comment_path' for #<#<Class:0x007fb58e4dbde0>:0x007fb58d0453e0>
Rails incorrectly guessed the the route helper method would be comment_path (unfortunately, it knows nothing of your route configuration). The correct call would be post_comment_path(@comment.post, @comment)
, which returns /posts/8/comments/4
. However, that requires the calling code to know too much about comments.
My solution
I wanted a way to “teach” my application how to resolve my various models into URLs. The idea is inspired by FubuMVC’s UrlRegistry (which was originally inspired by Rails’ url_for functionality…). I came up with SmartUrlHelper. It provides a single method: smart_url_for
, which is a wrapper around url_for
. The difference is that you can register “handlers” which know how to resolve your edge cases.
To solve my example problem above, I’d add the following code to config/initializers/smart_urls.rb:
SmartUrlHelper.configure do |url|
url.for ->model{ model.is_a?(Comment) } do |helpers, model|
helpers.post_comment_path(model.post, model)
end
end
Now I can call smart_url_for(@post)
or smart_url_for(@comment)
and get the expected URL. The comment
is resolved by the special case handler, and the post
just falls through to the default url_for
call. Note that in this example, I use instance variables named @post and @comment, which implies I know the type of object stored in the variable. In that case, smart_url_for
is just a convenience. However, consider a scenario where you have generic code that needs to build a URL for any model passed to it (like the form_for helper). In that case, something like smart_url_for
is a necessity.
Feedback
First, does Rails already have this functionality built-in, or is there an accepted solution in the community? If not, what do you think of this approach? I’d welcome suggestions for improvement. Particularly, I’m not wild about storing the handlers in the Rails.config object, but didn’t know a better way to separate the configuration step (config/initializer) from the consuming step (calls to smart_url_for
). So far, it is working out well on my project.