homeASCIIcasts

232: Routing Walkthrough Part 2 

(view original Railscast)

Other translations: It Es

In this episode we’ll continue from last week and carry on looking at the internals of Rails 3’s routing code. At the end of the last episode our application’s routing file looked like this.

/config/routes.rb

Store::Application.routes.draw do
  match 'products', :to => ProductsController.action("index")
  match 'products/recent'
end

Last time we showed the internals of the match method and showed what goes on when we call match in the routes file but there are a number of other methods that we can call and we’ll take a look at some of them in this episode.

If we look in Rails 3.0’s source code we’ll find the routing logic in the actionpack/lib/actiondispatch/routing directory and we’ll be focussing on the Mapper class in this directory because, as we discussed last time, the block inside the routes file is scoped to this class. This means that any methods called within the block are called against an instance of Mapper and we can therefore call any of Mapper’s methods in our routes file.

The code in the Mapper class can be a little overwhelming. The code is long, at almost 1,000 lines, and complex but the good news is that this is the longest file related to routing in Rails so if you can grasp what’s going on in this file and understand how it works then you’ll have a pretty good idea as to how routing works in Rails.

In order to get a good overview of the code we’ll collapse it down using the code-folding facility in TextMate. Pressing Command-Option-0 will fold everything. We’ll then expand the root module ActionDispatch, its submodule Routing and finally the Mapping class itself to get an overview of its structure.

The structure of the Mapper class.

The first two items in the Mapper class are class definitions for Constraints and Mapping. We touched on both of these in the last episode but what’s worth noticing here is that the classes are nested under the Mapper class. This might seem strange if you’re new to Ruby and you could well be wondering why you’d nest classes like this. There’s no magic going on behind the scenes here; the Constraints class is completely separate from the Mapper class. The reason this is done is that nesting the classes defines the namespace for the Constraints and Mapping classes so that they will appear under the Mapper namespace. There’s no inheritance or shared behaviour when you nest classes like this in Ruby.

Moving down the class we have two class methods, self.normalize_path and self.normalize_name. These are utility methods that are used throughout the class. Below that is a set of modules:

rails/actionpack/lib/action_dispatch/routing/mapper.rb

module Base...
module HttpHelpers...
module Scoping...
module Resources...
module Shorthand...
include Base
include HttpHelpers
include Scoping
include Resources
include Shorthand

These five modules are included into the Mapper class. The code in them is placed in modules merely as a way to organize the code in the class.

Base

We looked at the first module, Base, in the last episode. It contains the match method, the root method that uses match, and also a mount method that provides another way to map a Rack application to a URL.

rails/actionpack/lib/action_dispatch/routing/mapper.rb

module Base
  def initialize(set) #:nodoc:
  def root(options = {})
    match '/', options.reverse_merge(:as => :root)
  end
  def match(path, options=nil)...
  def mount(app, options = nil)...
  def default_url_options=(options)...
  alias_method :default_url_options, :default_url_options=
end

HttpHelpers

The next module is HttpHelpers and this is where the get, post, put and delete methods are defined. These methods are used to map routes to certain types of requests.

rails/actionpack/lib/action_dispatch/routing/mapper.rb

module HttpHelpers
  def get(*args, &block)
    map_method(:get, *args, &block)
  end
  def post(*args, &block)
    map_method(:post, *args, &block)
  end
  def put(*args, &block)
    map_method(:put, *args, &block)
  end
  def delete(*args, &block)
    map_method(:delete, *args, &block)
  end
  def redirect(*args, &block)...
  private
  def map_method(method, *args, &block)
     options = args.extract_options!
     options[:via] = method
     args.push(options)
     match(*args, &block)
     self
   end
end

All of these methods call the private map_method method. This method sets the :via option according to the method passed in and then calls match. You’ll notice in the routing code that a lot of the methods delegate to the match method, passing in and customizing certain options beforehand. So if we only want a route to respond to a GET request we could do it this way, using the via option.

match 'products/recent', :via => :get

In practice we’d do this by using the shorter get method, which will create a route with the same option.

get 'products/recent'

The post, put and delete methods work in much the same way as the get method for the other request types. The redirect method, however, is interesting as it is very different from the others and returns a Rack application.

rails/actionpack/lib/action_dispatch/routing/mapper.rb

def redirect(*args, &block)
  options = args.last.is_a?(Hash) ? args.pop : {}
  path      = args.shift || block
  path_proc = path.is_a?(Proc) ? path : proc { |params| path % params }
  status    = options[:status] || 301
  lambda do |env|
    req = Request.new(env)
    params = [req.symbolized_path_parameters]
    params << req if path_proc.arity > 1
    uri = URI.parse(path_proc.call(*params))
    uri.scheme ||= req.scheme
    uri.host   ||= req.host
    uri.port   ||= req.port unless req.standard_port?
    body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)
    headers = {
      'Location' => uri.to_s,
      'Content-Type' => 'text/html',
      'Content-Length' => body.length.to_s
    }
    [ status, headers, [body] ]
  end
end

The method returns Rack application by returning an array comprised of a status, some headers and a body. The status defaults to 301 which will make the browser perform a simple 301 Moved Permanently redirect. We can use this redirect method directly inside our routes file if we want one URL to redirect to another. In our routes file we already have a route that uses the :to parameter and this parameter takes a Rack application.

match 'products', :to => ProductsController.action("index")

As the redirect option returns a Rack app we can use it here to redirect to a new URL like this:

match 'products', :to => redirect("/items")

This feature becomes really useful when you’re changing the URLs in your application but still want to support legacy URLs. You can use redirect to redirect these to their new equivalents.

Shorthand

The next modules listed are Scoping and Resources but we’ll come back to those shortly. Instead we’ll take a look at the Shorthand module. This is an interesting module that redefines the match method, which was defined back in the Base module. This match method supports a different syntax for the options that you can pass to it. The shorthand method is an alternative way to write the :to option in a route such as the redirect route we wrote above.

match 'products', :to => redirect('/items')

This is a common thing to do in a routes file and the shorthand method lets us write the route with a simple hash made up of the route and whatever that route should point to. As with the full route syntax we can append parameters to the end of the route.

match 'products' => redirect('/items')

Shorthand’s match method sets the :to parameter when it isn’t already set. It then calls super but as Mapper doesn’t inherit from another class what does super call in this case?

rails/actionpack/lib/action_dispatch/routing/mapper.rb

module Shorthand
  def match(*args)
    if args.size == 1 && args.last.is_a?(Hash)
      options  = args.pop
      path, to = options.find { |name, value| name.is_a?(String) }
      options.merge!(:to => to).delete(path)
      super(path, options)
    else
      super
    end
  end
end

When we use super like this Ruby will look for a method with the same name that was defined in an earlier module. The Shorthand module is defined last in the list of modules that are included in Mapper so Ruby will look through the earlier modules for a match method and delegate to that. In this case it will call match in the Base module.

This technique is used often in the Rails 3 source code. Earlier versions of Rails used alias_method_chain to override specific behaviour, but now in Rails 3 we can just use super.

Resources

That’s it for the Shorthand module; next we’ll take a look at Resources. As you’d expect this module contains the resources method and all of its associated methods. We use resources in our routes file to create RESTful routes.

rails/actionpack/lib/action_dispatch/routing/mapper.rb

def resources(*resources, &block)
  options = resources.extract_options!
  if apply_common_behavior_for(:resources, resources, options, &block)
    return self
  end
  resource_scope(Resource.new(resources.pop, options)) do
    yield if block_given?
    collection_scope do
      get  :index if parent_resource.actions.include?(:index)
      post :create if parent_resource.actions.include?(:create)
    end
    new_scope do
      get :new
    end if parent_resource.actions.include?(:new)
    member_scope  do
      get    :edit if parent_resource.actions.include?(:edit)
      get    :show if parent_resource.actions.include?(:show)
      put    :update if parent_resource.actions.include?(:update)
      delete :destroy if parent_resource.actions.include?(:destroy)
    end
  end
  self
end

This method is fairly complex but if you look at the general structure of it it makes sense. There are a couple of collection methods, get :index and post :create; there is a get :new method and finally get :edit, get :show, put :update and delete :destroy. You should recognize these as the famous seven RESTful actions and these are created for a controller when you call resources on it in the routes file.

Note the first line in the method’s resource_scope block. If a block is passed to the method then the method will yield to that block before it creates the RESTful actions. This gives us the ability to create our own actions in the routes file. For example we could add a new collection route that returns the discounted products.

/config/routes.rb

Store::Application.routes.draw do
  match 'products', :to => redirect('/items')
  get 'products/recent'
  resources :products do
    collection do:
      get :discounted
    end
  end
end

The code inside the block passed to resources in the routes above will be executed by the yield call in resource_scope and the standard RESTful actions will be defined afterwards. We can use similar code in the block above to that in the resources method in the Rails source code to define our custom actions.

Looking at the blocks in the routes file above you might think that the object is changing every time we create a new block but this isn’t the case. We’re still working with the same Mapper object we worked with in the beginning so calling get in the innermost blocks is exactly the same as calling it in the outermost. We are dealing with a different scope, though, and we’ll discuss scopes shortly.

If you take another look back at the resources method from Rails’ source code you’ll see that the code uses a collection_scope call when it defines the index and create actions but inside our routes file we just use collection. What’s the difference? Well, not much. If we look at the collection method in the Mapper class we’ll see that it delegates to collection_scope.

rails/actionpack/lib/action_dispatch/routing/mapper.rb

def collection
  unless @scope[:scope_level] == :resources
    raise ArgumentError, "can't use collection outside resources scope"
  end
  collection_scope do
    yield
  end
end

Let’s take another quick look at our routes file.

/config/routes.rb

Store::Application.routes.draw do
  match 'products', :to => redirect('/items')
  get 'products/recent'
  resources :products do
    collection do:
      get :discounted
    end
  end
end

Both calls to get in the code above call the same method but the one inside the collection block will assume some additional behaviour according to how it is scoped inside the resources and collection blocks.

If we take a look back in the Resources module we’ll see a familiar-looking method, match. This redefines the match method and adds some additional behaviour based on resources.

rails/actionpack/lib/action_dispatch/routing/mapper.rb

def match(*args)
  options = args.extract_options!.dup
  options[:anchor] = true unless options.key?(:anchor)
  if args.length > 1
    args.each { |path| match(path, options.dup) }
    return self
  end
  on = options.delete(:on)
  if VALID_ON_OPTIONS.include?(on)
    args.push(options)
    return send(on){ match(*args) }
  elsif on
    raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
  end
  if @scope[:scope_level] == :resources
    args.push(options)
    return nested { match(*args) }
  elsif @scope[:scope_level] == :resource
    args.push(options)
    return member { match(*args) }
  end
  action = args.first
  path = path_for_action(action, options.delete(:path))
  if action.to_s =~ /^[\w\/]+$/
    options[:action] ||= action unless action.to_s.include?("/")
    options[:as] = name_for_action(action, options[:as])
  else
    options[:as] = name_for_action(options[:as])
  end
  super(path, options)
end

If we look about halfway down the code above you’ll see the line that checks the current scope to see if it is resources. If it is some different behaviour is added. The logic is fairly complex; all you need to know is that the Resources module redefines the match method. Note that at the end it calls super so that the match method in Base is called. Remember that get calls match and this is where the additional functionality is located for dealing with get and other methods that are defined within resources.

Scoping

We’re now down to the last method in our Mapping class: Scoping. Whenever there’s a block inside your routes file there’s a call to Scoping’s scope behind the scenes. That means that it will define some additional behaviour for the code inside that block.

Along with the scope method there are a number of other methods, all of which delegate to scope.

rails/actionpack/lib/action_dispatch/routing/mapper.rb

def initialize(*args) #:nodoc:
  @scope = {}
  super
end
def controller(controller, options={})
  options[:controller] = controller
  scope(options) { yield }
end
def namespace(path, options = {})
  path = path.to_s
  options = { :path => path, :as => path, :module => path,
              :shallow_path => path, :shallow_prefix => 			path }.merge!(options)
  scope(options) { yield }
end
def constraints(constraints = {})
  scope(:constraints => constraints) { yield }
end
def defaults(defaults = {})
  scope(:defaults => defaults) { yield }
end

These methods are all fairly simple and all delegate to a more generic method having first set some options. For example defaults calls scope after setting some defaults options. and likewise constraints calls scope with some constraints options. The namespace method is a little more complex but does essentially the same thing. The module also has an initialize method which just creates a @scope instance variable and sets it to be an empty hash. You might be wondering what an initialize method is doing here as modules can’t be instantiated. This is true, but in this case we’re just overriding a method behaviour. When the Scoping module is included in the Mapper class this initialize method will override the current initialize method, add the @scope variable and then call super.

Finally we have the scope method itself and this is where all of the work takes place. There’s a lot of complexity in this method but all it is essentially doing is filling up the @scope variable with some information based on the options that are being passed into the scope. The method merges the options using the a number of private methods in the module. All it does is store up the scope information so that it can be used later on inside whatever match call you have. Essentially it adds additional functionality based on the current scope.

That’s basically how blocks inside the routes file work. If we define routes like this:

/config/routes.rb

Store::Application.routes.draw do
  controller :products do
    match #...
  end  
end

Whenever we call match in the controller block above (and remember it delegates to scope) the controller option will automatically be supplied in there.

That’s it for this episode. I hope it’s given you some idea what’s the methods inside the routes file are doing. Even though you have a large number of methods to choose from in the routes file most of them are really simple delegating to either match or scope passing in some additional options.