#232 Routing Walkthrough Part 2
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.
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 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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.


