This post originated from an RSS feed registered with Ruby Buzz
by Patrick Lenz.
Original Post: Special characters and nested routes
Feed Title: poocs.net
Feed URL: http://feeds.feedburner.com/poocsnet
Feed Description: Personal weblog about free and open source software, personal development projects and random geek buzz.
I might be the only crazy guy on the planet using strings as the primary record identifier in the URI and also allowing a bunch of special characters in it. And just in case I am, please move along. I’ll just keep this article around as a reference for myself.
Say you’ve got a Rails web app from the early days. Way back when REST and Rails haven’t been spending day and night together. Back when we just had named routes.
But actually, back when it was already popular to occasionally use something different from the numeric primary key to identify a resource in a URL. Example:
/profile/show/scoop
Well, as far as that would actually resemble a URI of my own profile, some people are way fancier with their nicknames and the punctuation therein. The most prominent punctuation character is probably the period (.).
Back in the days, that was all dandy. Well, that was then and this is now.
Meet the Resource
With restful routes, we don’t need no stinkin’ named routes (for the most part). A simple declaration like
# config/routes.rb
map.resources :users
and we get a boatload of useful helpers such as user_path and new_user_path generating the appropriate URIs for us. But I digress.
Rewiring our application to use restful routes for the User model (after having seen the light), all’s well until you try to access a URI like
/users/mr.piggy
Eek! Rails interprets the period as a designation of format, like in /users.xml to offer an XML representation of the requested resource.
Now, who wants a piggy representation of a user?
To allow a period in the params[:id] part of the URL (which, after all, is what the username ends up in after routing is done chopping apart the requested URI), Rails 2 will have a way to re-use the :requirements parameter as an argument to map.resources, like so:
(Note: This has been committed to the Rails trunk back in February of 2007 as changeset 6232. I backported it as a patch to Rails 1.2 residing locally in my application (so I didn’t have to directly modify the Rails code. If there’s an interest in that I’ll post it as a separate article in a few days.)
Boom, now /users/mr.piggy nicely ends up having a params[:id] value of mr.piggy (instead of a value of mr and a failing attempt to render a piggy format).
What about nesting?
But our journey doesn’t end here. Coincidentally, if a site has users, those users tend to interact with it (in a true Web 2.0 fashion, you know).
Adding further to routes.rb, we now have a routing configuration like this
# config/routes.rb
map.resources :users,
:requirements => { :id => %r([^/;,?]+) } do |users|
users.resources :interests
users.resources :buddies
end
Easy, right? Trying to access a URL like /users/mr.piggy/buddies throws an exception though.
No route matches "/users/mr.piggy/buddies" with {:method=>:get}
The fix would be to add the :requirements constraint to all the nested routes as well.
Such a nasty repetition. Especially considering we might add a bunch of additional resources in there. Object#with_options to the rescue. Here’s the refactored version:
# config/routes.rb
map.resources :users,
:requirements => { :id => %r([^/;,?]+) } do |users|
users.with_options :requirements => {
:id => %r([^/;,?]+)
} do |users_requirements|
users_requirements.resources :interests
users_requirements.resources :buddies
end
end
Much better. Accessing /users/mr.piggy/buddies works as expected now, routing to BuddiesController#index with params[:user_id] having a value of mr.piggy, which you can use in a before filter to setup the parent resource.
Final words
It’s debatable whether or not it’s a good idea to allow punctuation characters in the URI. In my case, reworking parts of an existing application with an existing user base of several hundred thousand registered users I didn’t have an option but to comply.