There’s a problem with Rails resourceful routes. It is very easy to generate standard resourceful routes; it is equally easy to match incoming fancy URLs. But DRYly generating fancy URLs is a lot harder than suspected.
Matching fancy incoming routes is easy enough:
get '/posts/:year/:month/:id' => 'posts#show'
Our application will now correctly match URLs like /posts/2011/3/1-hello-world
, but see what happens when we try to generate a polymorphic route:
link_to(post.title, post)
# => <a href="/posts/1">hello, world</a>
This is not what we want. Of course, we could set :year
and :month
parameters manually, but that’s not very DRY. We could also try to override the post_url
and post_path
methods, but this does not cover all use cases.
Hypothetical solution
Ideally, we would like to return a hash of parameters for the to_param
method:
class Post
def to_param
{ year: created_at.year,
month: created_at.month,
id: id }
end
end
Rails would then have to interpolate these params into the generated URL. Alas, this doesn’t work.
Actual, hacky solution
We can hack around it, tough, by overriding url_for
:
class ApplicationController
def url_for(options = {})
obj = options[:_positional_args][0]
if obj === Post
options[:_positional_keys] = [
obj.year,
obj.month,
obj
]
end
super(options)
end
helper_method :url_for
end
This works fine for the most part – despite being a terrible hack. The helper_method :url_for
line does, however, override ActionView
’s normal behaviour of generating only paths instead of full URLs. To get around this, we can create a new helper method that uses the controller method:
module ApplicationHelper
def url_for(options = {})
return super unless options.is_a? Hash
controller.url_for(
options.reverse_merge(only_path: true)
)
end
end
Now all URLs are properly generated:
link_to post.title, post
# => <a href="/2011/2/1-hello,world">Hello, world</a>
A note of warning
The code in these examples is extremely simplified. Being a hack, you need to add some proper checks for argument types and the contents of the :_positional_args
and :_positional_keys
options. Also, you can probably not depend on this behaviour remaining on subsequent versions of Rails. But it still just might come in useful some time…