homeASCIIcasts

231: Routing Walkthrough 

(view original Railscast)

Other translations: It Es

This week’s episode will be a little different. We’re going to dive into the internals of Rails 3 and take a look at some of its code, focussing on the code that handles routing. Below is the routes file for the Store application from the last episode. So that we can know what this routing code actually does we’re going to look at the Rails code that is called when it’s run.

/config/routes.rb

Store::Application.routes.draw do
  resources :products do
    resources :reviews
  end
  root :to => "products#index"
end

You might wonder what the point of this is and if it’s worth browsing around other code, but in my opinion it is as reading Ruby code written by other people is a great way of improving your own Ruby skills. You’ll see tricks and techniques that other people use that you can use later in your own code. Reading the Rails source code is also a good way to learn how to use Rails better and in this case we may well find better ways to write the routes file. If you’re trying to debug a problem or optimize some code in one of your projects or if you’re even considering contributing to Rails then learning its internals is a great way to do so.

Getting Started

This episode covers some more advanced features so we’ll assume that you already know how routing works in Rails 3. If you don’t, or if you want to refresh your knowledge then you’re encouraged to watch or read episode 203 first as the routing syntax is quite different from Rails 2’s.

The Rails source code is hosted on Github and it’s easy to clone the repository so that you can browse through it. We just need to run

$ git clone git://github.com/rails/rails.git

The master branch of the repository we downloaded is for Rails 3.1, which is the version currently in development but we want to look at the same version that our application was written with. We can switch to the version tagged 3.0.0 by running

$ git checkout v3.0.0

If we open up the Rails directory we’ll see that Rails is made up from many different parts. Anything related to controllers, views or routes is located in the actionpack directory so it’s this part of the code that we’re interested in.

The directory structure for the Rails source code.

Under actionpack most of the code related to routing is under the lib/action_dispatch directory.

The directory structure for actionpack showing the routing files.

The routes and draw methods

Before we start looking at the Rails source code let’s go back to our application and look again at its routes file.

/config/routes.rb

Store::Application.routes.draw do
  resources :products do
    resources :reviews
  end
  root :to => "products#index"
end

The first line of the code above calls a method called routes on Store::Application, where Store is the name of the application. If we look in the application’s application.rb file we’ll see that the application name is defined there.

/config/application.rb

require File.expand_path('../boot', __FILE__)
require 'rails/all'
# If you have a Gemfile, require the gems listed there, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(:default, Rails.env) if defined?(Bundler)
module Store
  class Application < Rails::Application
     # Configure sensitive parameters which will be filtered from the log file.
    config.filter_parameters += [:password]
  end
end

The Store::Application class is where a lot of the application’s configuration takes place and it inherits from Rails::Application. Anything prefixed with the Rails namespace is usually defined in the railties directory in the Rails source code; the code for the Rails::Application class is defined in rails/railties/lib/rails/application.rb and in this file we’ll find the routes method.

rails/railties/lib/rails/application.rb

def routes
  @routes ||= ActionDispatch::Routing::RouteSet.new
end

This is the method that’s called in our application’s routes file and all it does is create a new ActionDispatch::Routing::RouteSet. The routes method returns this new RouteSet and in the first line of our routes file we then call draw on that. Let’s see what the draw method does.

In the Rails source code a class can often be found in a file with a similar name and the RouteSet class is no exception. In the class we’ll find the draw method.

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

def draw(&block)
  clear! unless @disable_clear_and_finalize
  mapper = Mapper.new(self)
  if block.arity == 1
    mapper.instance_exec(DeprecatedMapper.new(self), &block)
  else
    mapper.instance_exec(&block)
  end
   finalize! unless @disable_clear_and_finalize
   nil
end

The draw method takes a block as an argument. First it clears any existing routes, then it creates a new Mapper object, passing self (the RouteSet) to it. (We’ll take a look at the Mapper class shortly.) The method then checks the arity of the block, that is how many arguments have been passed to it. If an argument has been passed then this means that the routing file is using the Rails 2 routing syntax. This is done so that the application can work with Rails 2 routes which had a map variable passed to the block like this:

/config/routes.rb

Store::Application.routes.draw do |map|
  # Rails 2 routes...
end

Whether our application uses Rails 2 or Rails 3 style routing it calls instance_exec next. For Rails 3 routes the block is passed straight in as an argument, whereas if the application uses Rails 2 style routes a new DeprecatedMapper object is created first and this is passed in instead. This will execute the block as if it was inside the instance which means that inside the block in a routes file everything is called against a Mapper object. This is what gives us the cool domain-specific language syntax in Rails 3 routes where we can just call methods such as resources and not have call them against a specific object as we did in Rails 2 when we’d use map.resources.

The last thing that the draw method does is call finalize! which is defined lower down in the RouteSet class and which freezes the set of routes.

How a Route is Mapped

Now we know that everything inside a Rails 3 routes file is called against a Mapper object let’s use a simple route as an example and see how it is processed.

/config/routes.rb

Store::Application.routes.draw do
  match 'products', :to => 'products#index'
end

Let’s take a look at the Mapper class to see what the match method does. There are a number of methods called match in the class; the one we’re interested in is in the Base module.

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

module Base
  def initialize(set) #:nodoc:
    @set = set
  end
  def root(options = {})
    match '/', options.reverse_merge(:as => :root)
  end
  def match(path, options=nil)
    mapping = Mapping.new(@set, @scope, path, options || {}).to_route
    @set.add_route(*mapping)
    self
  end
  # other methods
end

To create a new Mapper object we need to pass in a RouteSet. When the match method is called in the routes file to create a new route this new route is added to the set. This is done by creating a new Mapping object and then calling to_route on it. Note also the root method, which is really simple. All it does is call match, passing in the root URL and then adding the :as => :root option so that it’s a named route. When you create a root URL in your routes, it’s just calling match behind the scenes.

Next we’ll take a look at the Mapping class, which is contained in the same mapper.rb file.

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

class Mapping #:nodoc:
  IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix]
  def initialize(set, scope, path, options)
    @set, @scope = set, scope
    @options = (@scope[:options] || {}).merge(options)
    @path = normalize_path(path)
    normalize_options!
  end
  def to_route
    [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
  end
  private 
  def normalize_options!
    path_without_format = @path.sub(/\(\.:format\)$/, '')
    if using_match_shorthand?(path_without_format, @options)
      to_shorthand    = @options[:to].blank?
      @options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1')
      @options[:as] ||= Mapper.normalize_name(path_without_format)
    end
    @options.merge!(default_controller_and_action(to_shorthand))
  end
  # other private methods omitted.
end

When the a Mapper is initialized some instance variables are set from the parameters that are passed in and then normalize_options! is called. The normalize_options! method checks to see if we’re using a certain shorthand syntax in the route and in the using_match_shorthand? method that’s called we pick up an interesting tip. There is a shorthand way of defining controller actions where we separate the controller and action names with a slash.

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

# match "account/overview"
def using_match_shorthand?(path, options)
  path && options.except(:via, :anchor, :to, :as).empty? && path =~ %r{^/[\w\/]+$}
end

If we have defined our route this way then the :to and :as options will be set for us depending on the name of the URL. Let’s demonstrate this back in our application’s routes file by adding another route that uses this shorthand syntax.

/config/routes.rb

Store::Application.routes.draw do
  match 'products', :to => 'products#index'
  match 'products/recent', :to => 'products#recent'
end

We have supplied a :to parameter to this new route but it is filled in automatically if we miss it out. The shortcut method will also automatically create an :as parameter as if we had added :as => :products_recent.

How Rack is Used in Routing

Back in the match method, once we’ve created a new Mapping object we call to_route on it. This method returns an array of options that are used to make a new route.

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

def to_route
  [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
end

The first four elements in the array above are values returned from calls to methods in the Mapper class.

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

def app
  Constraints.new(
    to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
    blocks,
    @set.request_class
  )
end
def conditions
  { :path_info => @path }.merge(constraints).merge(request_method_condition)
end
def requirements
  @requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements|
    requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
    @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
   end
end
def defaults
  @defaults ||= (@options[:defaults] || {}).tap do |defaults|
    defaults.reverse_merge!(@scope[:defaults]) if @scope[:defaults]
    @options.each { |k, v| defaults[k] = v unless v.is_a?(Regexp) || IGNORE_OPTIONS.include?(k.to_sym) }
  end
end

The first option is app and whenever you see something called app in the Rails source code the chances are that it refers to a Rack application. This app method returns a new Constraints object so let’s see if that is a Rack application.

The Constraints class is defined in the same mapper.rb file. It takes has a number of methods, one of which is called call and takes an environment as a parameter.

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

def call(env)
  req = @request.new(env)
  @constraints.each { |constraint|
    if constraint.respond_to?(:matches?) && !constraint.matches?(req)
      return [ 404, {'X-Cascade' => 'pass'}, [] ]
    elsif constraint.respond_to?(:call) && !constraint.call(*constraint_args(constraint, req))
      return [ 404, {'X-Cascade' => 'pass'}, [] ]
    end
  }
  @app.call(env)
end

The Constraints class does indeed look like a Rack application. An interesting thing about the Constraints class is that it overrides the self.new method and you might wonder why a class would do that when it has its own initialize method.

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

def self.new(app, constraints, request = Rack::Request)
  if constraints.any?
    super(app, constraints, request)
  else
    app
  end
end
attr_reader :app
def initialize(app, constraints, request)
  @app, @constraints, @request = app, constraints, request
end

The reason for this is performance. Constraints is a piece of Rack middleware, in other words it wraps another Rack application. The first argument passed to self.new is app, which is a Rack app and when the call method’s called, if any of the constraints are triggered it will return a 404, otherwise it will trigger the Rack application that it’s wrapping. The self.new method is a piece of performance tuning so that when it’s called Rails won’t allocate another object in memory, wrap it and use it, instead it will just return the initial Rack application itself.

Now back to the code that calls this. Note that the first two arguments are a Rack application and an array of constraints. This is called in the app method of the Mapping class. When creating a new constraint here we check to see that the to method responds to call. (The to method simply returns the :to option that was defined for the route.) If it does it’s a Rack application and we pass it in; if not then it passes in a new RouteSet::Dispatcher object with some default options.

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

def app
  Constraints.new(
    to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
    blocks,
    @set.request_class
  )
end

It’s this code that gives us the ability to pass a Rack application to a route like this:

  root :to => proc { |env| [200, {}, ["Welcome"]] }

There’s more about using Rack in routes in episode 222 [watch, read]. Being able to use it in routes gives us a lot of flexibility.

If we don’t pass a Rack application to the :to option then a new RouteSet::Dispatcher object is created. We’ll take a look at how that’s handled now.

The Dispatcher class handles passing a request to the correct controller. In its controller_reference method you can see the code where the correct controller is determined.

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

def controller_reference(controller_param)
  unless controller = @controllers[controller_param]
    controller_name = "#{controller_param.camelize}Controller"
    controller = @controllers[controller_param] =
       ActiveSupport::Dependencies.ref(controller_name)
  end
  controller.get
end

This class also has methods to do things like setting the default action to index if one hasn’t been specified and a dispatch method which calls the action itself and which returns a Rack application. What this means is that we can take any of our application’s controllers, call action on it passing in the name of the action and have a Rack application returned.

ruby-1.9.2-p0 > ProductsController.action("index")
 => #<Proc:0x00000100ec56c0@/Users/eifion/.rvm/gems/ruby-1.9.2-p0/gems/actionpack-3.0.0/lib/action_controller/metal.rb:172>

This is what happens behind the scenes when we pass in a route like this.

match 'products', :to => 'products#index'

The string products#index is converted to ProductsController.action("index") and that returns a Rack application. The string syntax is merely a shortcut way of doing the same thing.

There’s a lot more about routing we could cover here but this is a good place to stop. There are the resources methods that generate a number of routes, there are various methods that allow us to scope conditions and pass blocks to them so that we can cascade scopes but hopefully there’s enough here to encourage you to look into the routing code yourself.

Routing is one of the more complicated areas of Rails so if you’re a little intimidated by the complexity of the code here the don’t worry, it is difficult. Start with the some of the other parts of the Rails code first and get up to speed with that before tackling the more complex parts.