A Brief Look at New Routing Techniques in Rails 3

The release candidate for Rails 3 was made available about a week ago and I finally had a chance to get it installed and play around with it. It would likely take several pages to go through all the goodies and changes, and I haven’t even scratched the surface myself. So for this article let’s focus, in brief, on some of the new features in routing.

My first exposure to Rails and routes was in version 2. I struggled, not with the concept of routing, but with the syntax and an unnatural feeling that came with writing routes in Rails 2. After spending a few hours with routes in Rails 3, I struggle no more. Let’s take a look at some of what can be done.

The basic routing mechanism is achieved using match. This replaces the behavior of map.connect. Keep in mind that in Rails 3, there is no map object.

Each example will have a comment following it to show the route that is generated if displayed using rake routes.

match '/login' => 'login#new' # rake routes: /login {:controller=>"login", :action=>"new"}

This creates a route that maps the URI /login to LoginController::new. This match doesn’t restrict the HTTP method (get, post, etc) but we can easily do that using the :via keyword

match '/login' => 'login#new', :via => :get # rake routes: GET /login {:controller=>"login", :action=>"new"}

This route will only respond to HTTP GET. If you want to pass in more methods, use an array:

match '/login' => 'login#new', :via => [:get, :post] # rake routes: GET|POST /login {:controller=>"login", :action=>"new"}

Matches can be made to read even better by using HTTP verbs in place of match

get '/login' => 'login#new' # GET /login {:controller=>"login", :action=>"new"}

Like in Rails 2, matches can contain parameters and there is a formalized way for denoting optionality.

get '/photos(/:year/(/:month(/:day)))' => 'photos#display' # GET /photos(/:year(/:month(/:day))) {:controller=>"photos", :action=>"display"}

In this example, :year, :month and :day are defined as option using parenthesis. This type of route isn’t named by default. A name prefix can be specified using the :as keyword.

get '/photos(/:year/(/:month(/:day)))' => 'photos#display', :as => "photos" # photos GET /photos(/:year(/:month(/:day))) {:controller=>"photos", :action=>"display"}

Now photos_path(year, month, day) can be called with optional parameters to generate the URL.

The scope block allows options to be applied to all routes within the block.

1 2 3 4 
scope :controller => :foo do   match '/login' => :new       # /login {:controller=>"foo", :action=>"new"}   match '/logout' => :destroy  # /logout {:controller=>"foo", :action=>"destroy"} end

is equivalent to

1 2 
match '/login' => "foo#new"       # /login {:controller=>"foo", :action=>"new"} match '/logout' => "foo#destroy"  # /logout {:controller=>"foo", :action=>"destroy"}

Instead of using a scope block to define the controller for a group of routes, a controller block could be used instead

1 2 3 4 
controller :foo do   match '/login' => :new        # /login {:controller=>"foo", :action=>"new"}   match '/logout' => :destroy  # /logout {:controller=>"foo", :action=>"destroy"} end

When a controller is defined (either via scope or controller), the match path may be omitted provided a symbol is passed to match instead of a String.

1 2 3 
controller :foo do   match :login  # login_ /login(.:format) {:controller=>"foo", :action=>"login"} end

In this example, the path and route name are derived from the action. This combination results in the name “login_”, a full name of “login__path” (two underscores). It’s not clear whether this is a bug or intentional, though the effect doesn’t feel consistent.

The behavior occurs when using scope with a pathless match, though this isn’t surprising given that controller is implemented using scope.

1 2 3 
scope :controller => :foo do   match :login  # login_ /login(.:format) {:controller=>"foo", :action=>"login"} end

scope can optionally be passed a string to specify a path prefix. This may explain the odd naming scheme above.

1 2 3 
scope "admin", :controller => :foo do   match '/restart' => :new # admin_restart /admin/restart {:controller=>"foo", :action=>"new"} end

You might think you could omit :controller from scope and have it automatically specify the controller. This doesn’t work with scope, but effect can be accomplished using namespace.

1 2 3 
namespace "admin" do   match '/restart' => :new # admin_restart /admin/restart {:controller=>"admin", :action=>"new"} end

The namespace, controller and scope blocks can be nested to produce complex routes if desired

1 2 3 4 5 6 7 8 9 
scope "admin" do  controller :foo do    scope :via => :get      scope :as => "as_test"         match :bar # bar_as_test GET /admin/bar(.:format) {:controller=>"foo", :action=>"bar"}      end    end  end end

The resources block gives us the expected functionality of map.resources from Rails 2. Within a resources block, we have access to the member and collection blocks. Actions specified within a member block operate on a instance of a resource. Actions specified within a collection block are not tied to a specific instance (these actions could operate on the entire collection, but don’t have to).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 
resources :users do   # GET /users(.:format) {:controller=>"users", :action=>"index"}  # users POST /users(.:format) {:controller=>"users", :action=>"create"}  # new_user GET /users/new(.:format) {:controller=>"users", :action=>"new"}  # GET /users/:id(.:format) {:controller=>"users", :action=>"show"}  # PUT /users/:id(.:format) {:controller=>"users", :action=>"update"}  # user DELETE /users/:id(.:format) {:controller=>"users", :action=>"destroy"}  # edit_user GET /users/:id/edit(.:format) {:controller=>"users", :action=>"edit"}   member do    get :promote # promote_user GET /users/:id/promote(.:format) {:controller=>"users", :action=>"promote"}    get :demote # demote_user GET /users/:id/demote(.:format) {:controller=>"users", :action=>"demote"}  end  collection do    get :purge # purge_users GET /users/purge(.:format) {:controller=>"users", :action=>"purge"}  end end

The promote and demote routes are only available under instances of users. The purge route is available independent of any particular user instance.

match can received any object as a destination provided it has a call method that returns [ status-code, headers, [body-text]]. This means proc can be used to create an in-line response.

1 2 3 
 match '/proc-test' => proc { |env|     [ 200, { 'Content-Type' => 'text/plain' }, ["It works pretty good"] ]   }

If writing in-line handlers seems like a bit much, an object instance could be passed in instead:

1 2 3 4 5 6 7 8 9 10 11 12 
class Foobar   def call(env)     status = 200     headers = { 'Content-Type' => 'text/plain' }     body = "Foobar is good"     [ status, headers, [body]]   end end  TestApp::Application.routes.draw do   match '/foobar' => Foobar.new  end

The routing will fail if the Content-Type header is missing.

I recommend reading the source code actionpack-3.0.0.rc/lib/action_dispatch/routing/mapper.rb for the nitty gritty details on how all this works. Other source files in the same directory path are worth taking a look at too.

Happy routing!