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!