jay's tags:
jay reads (3):
This post assumes an understanding routes and their basic usage. Here is a good resource to get started with routes if you need it. In this post I want to explore the mechanism used to match routes to paths.

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


del.icio.us Digg reddit StumbleUpon

Comments

  • silverwhisper said on Apr 30, 2007....
    OK, jay, that's just too hardcore for my feeble dilettante self.

    ed
  • jay said on May 01, 2007....
    :)
    I need to clean it up a bit.

Comment on "ruby on rails routes"

programming ruby on rails (Click to add tags below)

(Separate tags using commas, for example: New York, dating, vegetarian)
Comment Anonymously

Subscribe to the SoulCast Newsletter To Receive the Best Uncensored Blogs About Love, Sex, Relationships, God, Politics, and More.


Ever wonder what people really think and how they really live?

Read about the real lives of regular people like you whose powerful moving blogs will make you smile, cry, emotional, and warm inside.

Your FREE SoulCast newsletter is just moments away. Receive your first feel-good blog by entering your email address below.

First Name:
Your Email:


You can unsubscribe at any time with one click. We NEVER sell or share your email address with anyone. Period. close