Let's start with a simple controller and a route
class UserController < ApplicationController def posts render_text "posts for user: #{params[:user]}" end def tags render_text "tags for user: #{params[:user]}" end end
Now the route.
map.user '/:user', :controller => 'user', :action => 'posts'
Going to http://localhost:3007/jay will work as expected. Now the problem. What if a user has a . in their user name?
Go to http://localhost:3007/jay and you'll get a Routing Error. The . is a separator in rails. If you look at the rails code you'll see something like this.
module ActionController module Routing SEPARATORS = %w( / ; . , ? ) end end
At first we may be tempted to simply remove the . from the separators. I would not do this because we may want to use the features that require . as a separator. In fact, I know that I will use the related feature. Instead let's use :requirements to specify a regular expression that will be used to match :user.
map.user '/:user', :controller => 'user', :action => 'posts', :requirements => { :user => /.*/ }
This route will work, but made me uneasy because I wasn't sure of the precise mechanism used by routes to match urls. What happens if we want to add another route that will display all tags used by a user? The url should match this pattern ':user/tags'. Here is what our routes.rb will look like now.
ActionController::Routing::Routes.draw do |map| map.user '/:user/tags', :controller => 'user', :action => 'tags', :requirements => { :user => /.*/ } map.user '/:user', :controller => 'user', :action => 'posts', :requirements => { :user => /.*/ } end
Now let's run some tests to find out how the requirements behave with respect to the separators.
http://localhost:3007/jay
posts for user: jay
http://localhost:3007/jay/tags
tags for user: jay
http://localhost:3007/jay/foo
posts for user: jay/foo
This was a surprise to me. I assumed that routes used the built in separators to tokenize the path and then attempt to match the tokens to a route. This is wrong as we will see. Let's take a close look at the 3 tests above to see what is really happening.
The first url gives us what we expect. The second shows us that :user stops matching at /tags even though our :requirement tells :user to match everything.
In the third it shows us that the :requirements everything including characters after a /
I poked around the rails source and got it to output the code that is generated to determine matches for routes. Here are the respective recognize() methods that make the determination. Notice that there is no tokenization or splitting of the path. It's a pure regular expression.
def recognize(path, env={}) if (match = /\A\/(.*)\/tags\/?\Z/.match(path)) params = parameter_shell.dup params[:user] = match[1] if match[1] params end end def recognize(path, env={}) if (match = /\A\/(.*)\/?\Z/.match(path)) params = parameter_shell.dup params[:user] = match[1] if match[1] params end end
That's fascinating, but where do the separators come into play? Let's remove the :requirements and see what the recognize() method looks like.
ActionController::Routing::Routes.draw do |map| map.user '/:user/tags', :controller => 'user', :action => 'tags' map.user '/:user', :controller => 'user', :action => 'posts' end
Here are the respective recognize() methods.
def recognize(path, env={}) if (match = /\A\/([^\/;.,?]+)\/tags\/?\Z/.match(path)) params = parameter_shell.dup params[:user] = match[1] if match[1] params end end def recognize(path, env={}) if (match = /\A\/([^\/;.,?]+)\/?\Z/.match(path)) params = parameter_shell.dup params[:user] = match[1] if match[1] params
end end
Now everything makes sense. Using :requirements completely removes the implicit separators from the matching regular expression inserts the regexp from :requirements. Let's take a look at one more example that we would see with most uses of respond_to().
ActionController::Routing::Routes.draw do |map| map.user '/:user.:ext/tags', :controller => 'user', :action => 'tags' map.user '/:user.:ext', :controller => 'user', :action => 'posts' end def recognize(path, env={}) if (match = /\A\/([^\/;.,?]+)\.([^\/;.,?]+)\/tags\/?\Z/.match(path)) params = parameter_shell.dup params[:user] = match[1] if match[1] params[:ext] = match[2] if match[2] params end end def recognize(path, env={}) if (match = /\A\/([^\/;.,?]+)\.([^\/;.,?]+)\/?\Z/.match(path)) params = parameter_shell.dup params[:user] = match[1] if match[1] params[:ext] = match[2] if match[2] params end end



