Rails Routing from the Inside Out

This guide covers the internals of Rails routing. By referring to this guide, you will be able to:

  • Understand the overall layout of the routing code

  • Follow route recognition and route generation through the code

  • Identify spots where you can extend the routing code

Note The Rails routing code is complex and has many moving parts. It will probably require several readings of the Rails source to grasp what's going on. For best results, you should have a copy of the Rails source handy when reading this guide.

1. The Lay of the Land

The routing code is spread across ten separate files. Here's a quick overview as to what's in each one:

In actionpack/lib/action-controller/:

  • resources.rb - handles RESTful resources

  • routing.rb - The entry point for route processing

In actionpack/lib/action_controller/routing/:

  • builder.rb - constructs and returns routes based on given path & options

  • optimisations.rb - optimize for common cases to avoid slowness in route matching

  • recognition_optimisation.rb - optimize search through routes.rb

  • route.rb - defines ActionController::Routing::Route (a single route)

  • routeset.rb - defines ActionController::Routing::RouteSet (the set of all routes in your application)

  • routing_ext.rb - utility methods and extensions

  • segments.rb - defines classes to represent route segments (parts of a route)

2. Processing the Routing DSL

Have you ever thought about what's really going on when you define routes in a Rails application? Most developers focus on the actual route definitions, but the part that wraps around the routes is where the journey into understanding the routing code begins. Take a look at it now, in the routes.rb file in any Rails application:

ActionController::Routing::Routes.draw do |map|
  # your routes here...
end

The process of handling this file begins in the Rails initialization code (in railties/lib/initializer.rb). A great deal of initialization goes on before it gets to routing, because (among other things) plugins have to be given a chance to add to the routing DSL before it's processed. But eventually, there's a call to initialize_routing:

def initialize_routing
  return unless configuration.frameworks.include?(:action_controller)
  ActionController::Routing.controller_paths = configuration.controller_paths
  ActionController::Routing::Routes.configuration_file = configuration.routes_configuration_file
  ActionController::Routing::Routes.reload
end

Rails needs to do some prep work before it's ready to parse the routing configuration file. First, it looks at configuration.controller_paths. Earlier in the initialization process, Rails sets this to be an array that includes the default app/controllers path. If you are running in the development environment (and only then), Rails adds the directories under /railties/builtin to this array. Currently the only built-in controller is the one that handles /rails_info/properties when you're in development mode. In the process of evaluating routes, the Routing class uses this array of paths to determine which controllers are available to handle requests.

The next step is to tell the Routes object where to find its configuration file - in most cases, this is config/routes.rb, but if you really wanted to you could change this; it's set up by default_routes_configuration_file in initializer.rb. After the initial setup is complete, initialize_routing calls Routes.reload to actually process the file. The reload method (which actually belongs to the RouteSet class, as you'll learn shortly) does some timestamp and other checking to make sure that it doesn't reload the file unnecessarily, and ultimately loads the configuration file to kick off the processing of the DSL.

Tip If the route file is missing or empty, Rails will add :controller/:action/:id as the last-chance default route anyhow.

As you look at the routing code, you're going to see several important classes and objects that handle the process. The most sweeping of these are RouteSet, Routes, and Mapper.

  • RouteSet is a class that represents a set of routes, parsed into a format that can be used for route recognition and route generation.

  • Routes is an instance of RouteSet that holds the actual parsed routes for your application (in the routes and named_routes collections)

  • Mapper is a class that knows how to turn a route definition (a line of code written in the routing DSL) and turn it into a parsed route that can be stored in the Routes object. In your routes.rb file, map is an instance of Mapper.

Note Despite the capitalization, Routes is not a class. Rather, it's a constant that refers to an instance of RouteSet.

2.1. The Routes object

ActionController::Routing::Routes is instantiated in the Routing module. The actual code is in action_controller/routing.rb:

module ActionController
  module Routing
  # ...
    Routes = RouteSet.new
  end
end

Open up action_controller/routing/route_set.rb and you'll find the RouteSet class. Here's the draw method that gets called from your routes.rb file:

def draw
  clear!
  yield Mapper.new(self)
  install_helpers
end

So, at the 20,000 foot level, this is pretty simple. RouteSet#draw does three things:

  1. Call clear! to get rid of any existing routes

  2. Create a new instance of ActionController::Routing::RouteSet::Mapper, and yield it to the block

  3. Call install_helpers to make named routes available to controllers and views

When this process is finished, Routes.routes and Routes.named_routes will contain information derived from every call to a method of map in your routes.rb file.

2.2. Mapping Regular and Named Routes

The next stop on the journey is to take a look at Mapper. Remember, this is the class you're referring to when you use the map object in your route definitions. The methods of Mapper (also defined in route_set.rb) should look pretty familiar to you:

  • connect takes a path and options, and adds a route

  • root takes options, and adds a route for the root level request

  • namespace handles routes that are collected in namespaces

  • named_route takes takes a name, path, and options, and creates a named route

You most likely won't call named_route directly. That's because Mapper defines method_missing to hook any arbitrary method you throw at it and turn it into a named route.

But wait! What about RESTful routes? It turns out that RESTful routes are implemented as an extension to Mapper. If you look in action_controller/resources.rb, you'll find that it defines a Resources module, and then hundreds of lines later ends its work with:

class ActionController::Routing::RouteSet::Mapper
  include ActionController::Resources
end

I'll return to RESTful routes later, but first things first: what does Mapper actually do with regular, root, and named routes?

2.2.1. Mapping Regular Routes

Mapper#connect turns around and passes its options back to RouteSet#add_route, another simple method:

def add_route(path, options = {})
  route = builder.build(path, options)
  routes << route
  route
end

add_route delegates most of its work to builder, then adds the route that it gets back to the routes collection and passes it back to its caller. builder in turn is a factory method that lazily instantiates an ActionController::Routing::RouteBuilder object. You'll find this object in action_controller/routing/builder.rb. And when you get that far, you'll see how building a route from the routing DSL actually works.

2.2.2. The RouteBuilder Class

The RouteBuilder class does the work of turning a path string and a hash of options into a Route object. To do this, it first decomposes the path into a set of segments, and then combines the segments together with the options and default values to create the route. Here's the build method that controls the entire process. It's worth looking at in some detail to get an idea of the sort of issues that the routing DSL has to deal with:

def build(path, options)
  # Wrap the path with slashes
  path = "/#{path}" unless path[0] == ?/
  path = "#{path}/" unless path[-1] == ?/

  path = "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options[:path_prefix]

  segments = segments_for_route_path(path)
  defaults, requirements, conditions = divide_route_options(segments, options)
  requirements = assign_route_options(segments, defaults, requirements)

  # TODO: Segments should be frozen on initialize
  segments.each { |segment| segment.freeze }

  route = Route.new(segments, requirements, conditions)

  if !route.significant_keys.include?(:controller)
    raise ArgumentError, "Illegal route: the :controller must be specified!"
  end

  route.freeze
end

This code first puts the passed path into a canonical form by making sure that it has leading and trailing slash characters, and by prepending any :path_prefix option. It then calls segments_for_route_path to break the route down, and divide_route_options and assign_route_options to handle all the various options that can appear in a route definition. The actual route is built by a call to Route.new.

After a sanity check, build then calls freeze on the new route. You can think of freeze, for the moment, as performing some further work to make the route ready for route recognition and route generation. You'll see the actual details of this preparation later.

Breaking the Path into Segments

The process of figuring out what a route is made up from starts in segments_for_route_path. This method turns the passed path into an array of Segment objects by calling segment_for repeatedly: each call to segment_for creates one Segment object and strips the processed section off of the path string.

The segment_for method uses a series of regex-based rules to figure out what sort of segment comes next in the path string:

  • If the next chunk is :controller, create a new ControllerSegment

  • If the next chunk starts with a colon, create a new DynamicSegment

  • If the next chunk starts with an asterisk, create a new PathSegment

  • If the next chunk consists of alphanumeric characters, create a new StaticSegment

  • If the next chunk is in the list of known separators (/ . ?), create a new DividerSegment

The various classes that represent route segments are defined in action_controller/routing/segments.rb, where they form an inheritance hierarchy:

Segment
  StaticSegment
    DividerSegment
  DynamicSegment
    ControllerSegment
    PathSegment

A single route will be represented by a collection of appropriate segments. For instance, consider this (rather contrived) route defintion:

map.connect ':controller/photo/:id/*other', :controller => 'photos', :action => 'unknown'

The route path will be broken up into this collection of segments (each shown with the portion of the route that they represent):

DividerSegment     /
ControllerSegment  :controller
DividerSegment     /
StaticSegment      photo
DividerSegment     /
DynamicSegment     :id
DividerSegment     /
PathSegment        *other
DividerSegment     /

You'll see these segments in action later in this guide, in the discussions of route generation and route recognition.

Handling Options

Splitting the route path into segments isn't the end of processing a line from routes.rb. The routing code still has to handle the route options. This process starts in divide_route_options The job of +divide_route_options is to split off three special hashes from the full options hash:

  • requirements holds any :requirements option, specifying the acceptable format for a segment

  • defaults holds any :defaults option, specifying the default value for a segment

  • conditions holds any :conditions option, specifying the conditions for a route to match (the only currently-recognized condition is :method)

In addition to just pulling out the labeled hashes, divide_route_options also has to handle defaults and requirements that appear outside of their respective hashes. For example, this is a legitimate route definition:

map.connect ':controller/:action/:person/:id/', :controller => 'photos', :action => 'send', :person => /\w+/

In this case, there are supplied defaults for the :controller and :action keys, and a supplied requirement for the :person key. The divide_route_options method handles this situation by inspecting each segment to see whether it can find a matching key in the options. If so, it looks at whether the value of the key is a RegExp to determine whether it represents a default or a requirement.

After collecting these special hashes, build turns around and calls assign_route_options, passing in the route segments, the defaults, and the requirements. assign_route_options takes the hash of requirements and the hash of defaults, and figures out which segment each member of each hash belongs to. The matching regular expressions and default values are then assigned to the respective segment objects. Any requirements that do not match a particular segment are passed back as the return value for the assign_route_options.

assign_route_options also has two other important jobs. First, it assigns default options (index as the default for any segment with the key :action, marking any :id segment optional). Second, it checks that any optional segments do not precede a required segment.

Building a Route object

At this point, build is hanging on to a collection of segments, a hash of requirements that don't apply to any particular segment, and a hash of conditions. All of this information is passed to Route.new to build the actual ActionController::Routing::Route object, which stashes the information away for later use.

2.2.3. Mapping Named Routes

When mapping a named route, Mapper.connect starts by passing its options back to RouteSet.add_named_route:

def add_named_route(name, path, options = {})
  name = options[:name_prefix] + name.to_s if options[:name_prefix]
  named_routes[name.to_sym] = add_route(path, options)
end

The processing here is straightforward. First, the code disambiguates the name of the route by adding any supplied name prefix on to the start of the supplied name. Then it hands off the route path and options to add_route - which you already know all about, because it's the same method that handles regular, un-named routes. There's a difference in the end result, though. When it's called to handle a regular route, add_route stores the result in the Mapper.routes collection. When it's called to handle a named route, add_routes stores the resulting route in the Mapper.named_routes collection.

While Mapper.routes is an array of routes, Mapper.named_routes is an instance of ActionController::Routing::RouteSet::Mapper::NamedRouteCollection. Why the difference? Because named routes need to be available as helper methods. When named_routes is initialized, part of the processing creates a new Module that it hangs on to:

def clear!
  @routes = {}
  @helpers = []

  @module ||= Module.new
  @module.instance_methods.each do |selector|
    @module.class_eval { remove_method selector }
  end
end

So at the end of initialization, @module is a "do nothing" module. But each time you add a route to named_routes, it both stores the route and does some additional work with it:

def add(name, route)
  routes[name.to_sym] = route
  define_named_route_methods(name, route)
end

The define_named_route_methods method sets up four methods each time you pass it a named route:

def define_named_route_methods(name, route)
  {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
    hash = route.defaults.merge(:use_route => name).merge(opts)
    define_hash_access route, name, kind, hash
    define_url_helper route, name, kind, hash
  end
end

That's nearly the end of the chain. The define_hash_access_route and define_url_helper methods do the actual work of adding methods to the anonymous module in named_routes by using module_eval:

def define_hash_access(route, name, kind, options)
  selector = hash_access_name(name, kind)
  @module.module_eval <<-end_eval # We use module_eval to avoid leaks
    def #{selector}(options = nil)
      options ? #{options.inspect}.merge(options) : #{options.inspect}
    end
    protected :#{selector}
  end_eval
  helpers << selector
end

def define_url_helper(route, name, kind, options)
  selector = url_helper_name(name, kind)
  # The segment keys used for positional paramters

  hash_access_method = hash_access_name(name, kind)

  # allow ordered parameters to be associated with corresponding
  # dynamic segments, so you can do
  #
  #   foo_url(bar, baz, bang)
  #
  # instead of
  #
  #   foo_url(:bar => bar, :baz => baz, :bang => bang)
  #
  # Also allow options hash, so you can do
  #
  #   foo_url(bar, baz, bang, :sort_by => 'baz')
  #
  @module.module_eval <<-end_eval # We use module_eval to avoid leaks
    def #{selector}(*args)
      #{generate_optimisation_block(route, kind)}

      opts = if args.empty? || Hash === args.first
        args.first || {}
      else
        options = args.extract_options!
        args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)|
          h[k] = v
          h
        end
        options.merge(args)
      end

      url_for(#{hash_access_method}(opts))
    end
    protected :#{selector}
  end_eval
  helpers << selector
end

The easiest way to see how this works is to look at an example. Consider this simple RESTful route:

map.resources :photos

Among other things, processing this route will generate helpers for the edit_photo route. Here's what those helpers look like:

def hash_for_edit_photo_path(options = nil)
  options ? {:action=>"edit", :only_path=>true, :use_route=>:edit_photo, :controller=>"photos"}.merge(options) : {:action=>"edit", :only_path=>true, :use_route=>:edit_photo, :controller=>"photos"}
end

protected :hash_for_edit_photo_path

def edit_photo_path(*args)
  opts = if args.empty? || Hash === args.first
    args.first || {}
  else
    options = args.extract_options!
    args = args.zip([:id]).inject({}) do |h, (v, k)|
      h[k] = v
      h
    end
    options.merge(args)
  end
  url_for(hash_for_edit_photo_path(opts))
end

protected :edit_photo_path

def hash_for_edit_photo_url(options = nil)
  options ? {:action=>"edit", :only_path=>false, :use_route=>:edit_photo, :controller=>"photos"}.merge(options) : {:action=>"edit", :only_path=>false, :use_route=>:edit_photo, :controller=>"photos"}
end

protected :hash_for_edit_photo_url

def edit_photo_url(*args)
  opts = if args.empty? || Hash === args.first
    args.first || {}
  else
    options = args.extract_options!
    args = args.zip([:id]).inject({}) do |h, (v, k)|
      h[k] = v
      h
    end
    options.merge(args)
  end
  url_for(hash_for_edit_photo_url(opts))
end

protected :edit_photo_url

TODO: Worth going into more depth on the process?

2.2.4. Mapping the Root Route

If you use map.root in your routes.rb file, the corresponding route is processed by the root method of Mapper. This method is fairly simple. First, it checks to see if you've passed a single symbol into map.root. This catches cases where you're using a previously-defined named route as the root route, such as:

map.index :controller => "pages", :action => "main"
map.root :index

In this case, the root method looks up the corresponding named route among all the routes that have already been processed (which is why, if you're using this syntax, the named route must be defined above the root route) and merges any conditions from that route into the options for the root route. Whether the root route uses a symbol or just a simple route, the final step of this method is to create a named route using the special name root:

named_route("root", '', options)

So in the end, setting up the root route is just a special case of creating a named route.

2.3. Mapping RESTful Routes

Remember, ActionController::Resources (defined in action_controller/resources.rb) mixes itself into Mapper. That means that if you have a call to map.resources or map.resource in your routing file, it gets handled in the resources code. Here are the top-level entry points for those ways of mapping a route:

def resources(*entities, &block)
  options = entities.extract_options!
  entities.each { |entity| map_resource(entity, options.dup, &block) }
end

# ...

def resource(*entities, &block)
  options = entities.extract_options!
  entities.each { |entity| map_singleton_resource(entity, options.dup, &block) }
end

Note Pay close attention to naming here: map.resources (plural) ends up in map_resource (singular), while map.resource ends up in map_singleton_resource.

The extract_options! method is a Rails idiom provided by Active Support, which encapsulates the pattern of extracting an option hash from a variable number of arguments. This allows the routing DSL to handle routes that define more than one RESTful route:

map.resources :books, :photos, :videos, :options => { :name_prefix => nil }

The routing code will end up passing a duplicate of the options to map_resource (in this case) for each of the three routes to be defined.

The &block option contains any nested resources for the resource currently being processed.

2.3.1. Mapping Regular RESTful Routes

The map_resource method breaks up the work of building up a RESTful route to six steps:

  1. Create an instance of the Resource class

  2. Use map_collection_actions to set up collection routes

  3. Use map_default_collection_actions to set up default collection routes

  4. Use map_new_actions to set up new routes

  5. Use map_member_actions to set up member routes

  6. Use map_associations to set up nested resources

After all of this work is done, it takes any incoming block, sets options to account for the nesting, and recursively maps the contents. It does this by using with_options:

if block_given?
  with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
end

You might be expecting a with_options method on Mapper or Resource, especially if you're only familiar with its use in routes.rb, where it can help define a group of routes with shared options:

with_options :order => 'created_at', :class_name => 'Comment' do |post|
  post.has_many :comments, :conditions => ['approved = ?', true], :dependent => :delete_all
  post.has_many :unapproved_comments, :conditions => ['approved = ?', false]
  post.has_many :all_comments
end

But, in fact, with_options is a general-purpose extension that Rails tacks on to the Object class. Its job is to take a hash of common options and apply it to everything within the contained block when executing the statements in the block. So, in this case, it supplies the common options (path and name prefix, namespace, shallow, only and except) for each of the nested routes, and then sends the individual routes off to be processed.

The Resource Class

The routing code initializes an instance of the Resource class by passing it the name of the entities being routed (like "photos") and the options extracted from the original map.resources line. This triggers quite a bit of initial processing. First, it determines the plural and singular names of the resource (this is where the :singular option comes into play if you supply one). It also comes up with the path_segment representing the resource; this will be either the plural name, or the :as option if the route contains one. All of these, as well as the unprocessed options, are stored as methods of the Resource object for future reference.

With this part of the initialization done, Resource is able to supply the name of the controller that this resource will do, which is important for much of the following processing. The controller name is lazily instantiated:

def controller
  @controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}"
end

By default, the name of the controller is the pluralized name of the resource, but you can change this with the :namespace and :controller options.

After this initial processing, initialize calls four more methods:

  • arrange_actions

  • add_default_actions

  • set_allowed_actions

  • set_prefixes

The arrange_actions method sets up the collection_methods, member_methods, and new_methods members of the Resource by processing (respectively) the :collection, :member, and :new options of the initial route. It uses a slick little method named arrange_actions_by_methods to convert these option hashes into a format more useful for further processing. For example, if your original route includes the option

:collection => { :search => :get, :chart => :get, :related => :any }

This is what will end up in collection_methods:

{:get => [:search, :chart], :any => [:related]}

The add_default_actions method then takes the member_methods has and adds :edit to the array of methods for the :get verb. Similarly, it adds :new to the array of :get actions in new_methods.

The set_allowed_actions method handles the :only and :except options for resource routes. It does this by setting up an :actions option, which defaults to containing the standard 7 actions:

DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy

def set_allowed_actions
  only    = @options.delete(:only)
  except  = @options.delete(:except)

  if only && except
    raise ArgumentError, 'Please supply either :only or :except, not both.'
  elsif only == :all || except == :none
    options[:actions] = DEFAULT_ACTIONS
  elsif only == :none || except == :all
    options[:actions] = []
  elsif only
    options[:actions] = DEFAULT_ACTIONS & Array(only).map(&:to_sym)
  elsif except
    options[:actions] = DEFAULT_ACTIONS - Array(except).map(&:to_sym)
  else
    # leave options[:actions] alone
  end
end

The set_prefixes method simply pulls :path_prefix and :name_prefix out of options and puts them into the path_prefix and name_prefix members of the Resource instance.

Using map_collection_actions to Set up Collection Routes

At this point, the Resource object has been initialized with its options all nicely sorted out into separate methods. The next step for map_resource is to call map_collection_actions with the Resource and its controller. How does it know which controller to use? Because the Resource object has a controller method that calculates the controller name, using the name of the resource as well as any passed :namespace or :controller options.

The map_collection_actions method is responsible for walking the list of collection actions (in Resource.collection_methods) and turning them into actual named routes. It does this by calling map_resource_routes repeatedly:

def map_collection_actions(map, resource)
  resource.collection_methods.each do |method, actions|
    actions.each do |action|
      [method].flatten.each do |m|
        map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action}", "#{action}_#{resource.name_prefix}#{resource.plural}", m)
      end
    end
  end
end

The map_resource_routes method in turn calls down into either map_named_route or map.connect for each action that this resource actually implements (as determined by the value of the :only and :except options):

def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil)
  if resource.has_action?(action)
    action_options = action_options_for(action, resource, method)
    formatted_route_path = "#{route_path}.:format"

    if route_name && @set.named_routes[route_name.to_sym].nil?
      map.named_route(route_name, route_path, action_options)
      map.named_route("formatted_#{route_name}", formatted_route_path, action_options)
    else
      map.connect(route_path, action_options)
      map.connect(formatted_route_path, action_options)
    end
  end
end

And now you're back on familiar ground - because map.named_route is exactly the same method that you saw earlier in this guide to construct "real" named routes, and map.connect is the method for constructing regular, un-named routes. If you think about what's happening here, you can see the overall strategy used by resources.rb to handle RESTful routes: use the information from routes.rb to figure out what traditional named routes would correspond to each implied member of the RESTful route, and then tell the regular routing code to construct that route. It's as if you layered one DSL on top of another.

The action_options_for method hard-codes options for the seven default RESTful routes (for example, that index requires the :get method).

Using map_default_collection_actions to Set up Default Collection Routes

In addition to any routes specified with the :collection option, each RESTful resource has two collection routes by default: index and create. Setting up these routes is the job of map_default_collection_actions.

The index route first spends a few lines of code to figure out the right method name, based on name_prefix, plural resource name, and whether the resource is uncountable. It then calls map_resource_routes, which (as you've just seen) turns this information into a pair of named routes. It also makes a second call to map_resource_routes to build the routes with the same name for the create route - these routes are distinct from those for the index route because action_options_for returns a different set of options for create.

Using map_new_actions to Set up New Routes

As you can probably guess by now, map_new_options sets up any "new record" routes, by iterating over resource.new_methods. At the minimum, this will contain the :new method (inserted there by add_default_actions), and it will also contain any other routes that were specified with the :new option in the original route definition. After doing the necessary work to figure out the proper name and path for each route, map_new_actions sends them down the map_resource_routes path to becoming named routes.

Using map_member_actions to Set up Member Routes

The next group of routes is handled by map_member_actions. This method first walks through resource.member_methods, sending each route it contains to map_resource_routes. It then uses special case handling to set up the standard show, update, and destroy routes.

Use map_associations to Set up Nested Resources

The map_associations method handles nested routes defined using the has_many and has_one operators, such as

map.resources :photos, :has_one => :photographer, :has_many => [:publications, :versions]

The map_associations method first grabs the :has_many option and sends it to mas_has_many_associations. This method processes the option differently depending on whether it is a Hash, an Array, or a Symbol or String - but ultimately all the code paths end up calling the resources method that we saw as the top-level entry to RESTful routing. Thus, nested resource routes can use all of the syntax of top-level resource routes (including the ability to contain more nested resources of their own).

After handling the has_many option, map_associations goes through the has_one option and sends each of the resources that it contains back to the top-level resource method, to be handled as a singleton resource.

2.3.2. Mapping Singular RESTful Routes

Processing a singular RESTful route, in map_singleton_resource, is quite similar to the processing you've already seen for a plural resource. There are six steps to the process:

  1. Create an instance of the SingletonResource class

  2. Use map_collection_actions to set up collection routes

  3. Use map_default_singleton_actions to set up default singleton routes

  4. Use map_new_actions to set up new routes

  5. Use map_member_actions to set up member routes

  6. Use map_associations to set up nested resources

After all of this work is done, it takes any incoming block, sets options to account for the nesting, and recursively maps the contents.

Most of the details of this process have already been covered for the case of plural RESTful routes. Only the SingletonResource class and the map_default_singleton_actions are unique to singleton resources.

The SingletonResource Class

The routing code initializes an instance of the SingletonResource class by passing it the name of the entity being routed (like "session") and the options extracted from the original map.resources line. This triggers quite a bit of initial processing. First, it determines the plural and singular names of the resource (in the singleton case, these are both simply the name of the entity, but the aliases are required by later processing). It also comes up with the controller name for the resource by pluralizing the singular name. All of these, as well as the unprocessed options, are stored as methods of the SingletonResource object for future reference.

SingletonResource is derived from Resource, and after this initial processing, it defers to the initialize method in Resource. Thus singleton routes go through the same arrange_actions, add_default+actions, and set_prefixes handling as do their plural cousins.

Using map_default_singleton_actions to Set up Default Singleton Routes

The other major difference between processing plural RESTful routes and processing singleton RESTful routes is that singleton routes do not have a separate index route. So instead of calling map_default_collection_actions, singleton routes call map_default_singleton_actions, which currently handles the single action create.

2.4. Installing Routes as Helpers

After Mapper has translated all of your route definitions into Route objects, the Draw method has one more task to perform: making the routes available everywhere that you might want to use them in your application. To do this, it calls RouteSet#install_helpers:

def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
  Array(destinations).each { |d| d.module_eval { include Helpers } }
  named_routes.install(destinations, regenerate_code)
end

This method first uses module_eval to reopen ActionController::Base and ActionView::Base and tell them to include helpers. It then calls install on the named_routes collection:

def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false)
  reset! if regenerate
  Array(destinations).each do |dest|
    dest.__send__(:include, @module)
  end
end

The @module variable contains a Module that's set up during the initialization of the NamedRoutesCollection. As you should recall from the discussion of named routes, each time you add a named route the collection creates four methods on this anonymous module. For example, if you're adding the edit route from map.resources :photos, Rails will add these four methods to the module:

  • hash_for_edit_photo_path

  • edit_photo_path

  • hash_for_edit_photo_url

  • edit_photo_url

By invoking send to mix this module into ActionController::Base and ActionView::Base, install_helpers makes all of these generated methods available to all of your controllers and views.

3. Recognizing Routes

Pause here a moment to take stock. So far, you've seen how routes.rb is processed by ActionController::Routing::RouteSet::Mapper. Just in case the trees have by now obscured the forest, the end result is a RouteSet object that exposes two collections: routes and named_routes.

But so far, you haven't seen how these collections are used, only how they're created. As you already know, Rails routing has two basic tasks to perform: it both connects incoming HTTP requests to the code in your application's controllers, and helps you generate URLs without having to hard-code them as strings. Both of those tasks depend on the existence of an initialized RouteSet object.

I'll look at route recognition - an essential part of the process of hooking up incoming requests to controllers and actions - first. Before that, though, it's time to go back and dig into one of the methods that I glossed over earlier: route.freeze.

Note The use of freeze is quite recent in Rails terms, coming in to the source code in late July 2008. Earlier versions of the routing code use a different strategy to generate the code for route recognition and route generation, though many of the pieces are the same.

3.1. Freezing Routes

After the Mapper creates a route, it freezes that route. Freezing is much more than declaring that the route should remain unchanged: it also does a great deal of processing to set up for use of the route. Here's the code:

def freeze
  unless frozen?
    write_generation!
    write_recognition!
    prepare_matching!

    parameter_shell
    significant_keys
    defaults
    to_s
  end

  super
end

In broad overview, here are the seven steps that this code takes to freeze a route:

  • write_generation! - Create code for generating a URL from the route

  • write_recognition! - Create code for recognizing the route from a URL

  • prepare_matching! - Figure out the requirements for :controller and :action

  • parameter_shell - Create a hash of extra keys for the route

  • significant_keys - Create an array of keys in the path and those that have requirements

  • defaults - Create a hash of keys in the route that have default values

  • super - Call Object.freeze to prevent any further modification to the route

For purposes of recognizing routes, the place to start is with write_recognition!. This method holds some of the "magic" of the Rails framework. When it's invoked, it actually creates a new method (named recognize) for the Route object:

# Write and compile a +recognize+ method for this Route.
def write_recognition!
  # Create an if structure to extract the params from a match if it occurs.
  body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
  body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"

  # Build the method declaration and compile it
  method_decl = "def recognize(path, env = {})\n#{body}\nend"
  instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
  method_decl
end

It might be instructive to look at some of the actual code that this method generates. For example, consider this simple named route:

map.activate '/activate/:id', :controller => 'accounts', :action => 'show'

Here's the corresponding generated recognize method:

def recognize(path, env = {})
  if (match = /\A\/activate(?:\/?\Z|\/([^\/.?]+)\/?)\Z/.match(path))
    params = parameter_shell.dup
    value = if (m = match[1])
              URI.unescape(m)
            else

            end
    params[:id] = value if value
    params
  end
end

Of course, more complex routes will generate more complex recognizers:

map.connect 'photo/:id/:format', :controller => 'photos', :action => 'show', :defaults => { :format => 'jpg' }, :requirements => { :id => /[A-Z]\d{5}/ }

This route generates this recognize method:

def recognize(path, env = {})
  if (match = /\A\/photo\/([A-Z]\d{5})(?:\/?\Z|\/([^\/.?]+)\/?)\Z/.match(path))
    params = parameter_shell.dup
    value = if (m = match[1])
              URI.unescape(m)
            else

            end
    params[:id] = value if value
    value = if (m = match[2])
              URI.unescape(m)
            else
              "jpg"
            end
    params[:format] = value if value
    params
  end
end

All of the generated recognize methods use this same general strategy:

  1. Use regular expressions to determine if the supplied path is a match for the route

  2. If so, process the contents of the route and return the route parameters

  3. Otherwise, don't return anything

Tip If you'd like to see the generated recognize methods for your own application, just add puts method_decl to write_recognition! directly after the line that creates method_decl. Then fire up your application in development mode and you'll see all the code in your console.

So, now that you know where you're going, it's time to see how you get there. The journey starts with the pieces that write_recognition! depends on: parameter_shell, recognition_extraction, and recognition_conditions.

3.1.1. The parameter_shell method

The first time that parameter_shell is called (which will be from freeze), it generates a hash of keys and values. On subsequent calls, it just returns the previously-generated hash. The has contains parameter values that are not in the route's path, but that belong in the hash if the route is recognized. For example, consider this route:

map.logout '/logout', :controller => 'sessions', :action => 'destroy'

When Rails recognizes this route, it needs to pass the :controller and :action keys back, even though they don't appear in the incoming URL. So in this case parameter_shell will contain {:controller ⇒ sessions, :action ⇒ destroy}. Like most of the route recognition (and route generation) code, this relies on information that was gathered when the routes.rb file was processed into Route objects - in this case, specifically, the requirements hash:

def parameter_shell
  @parameter_shell ||= returning({}) do |shell|
    requirements.each do |key, requirement|
      shell[key] = requirement unless requirement.is_a? Regexp
    end
  end
end

3.1.2. The recognition_extraction Method

The recognition_extraction method writes the code to extract the parameters from a route that matches the supplied URL.

def recognition_extraction
  next_capture = 1
  extraction = segments.collect do |segment|
    x = segment.match_extraction(next_capture)
    next_capture += segment.number_of_captures
    x
  end
  extraction.compact
end

Remember how the route was split up into segments, way back when it was first processed? This method loops through all of those segments, and asks each one to generate code to extract the information corresponding to that segment. This is handled by two methods of the Segment class, match_extraction and regexp_chunk. The details of these methods vary according to the class of a particular segment. Here's how the various implementations match up to the inheritance hierarchy within Segment:

Segment # Base class

  def match_extraction(next_capture)
    nil
  end

  StaticSegment # Fixed alphanumeric chunk

    def regexp_chunk
      chunk = Regexp.escape(value)
      optional? ? Regexp.optionalize(chunk) : chunk
    end

    DividerSegment # / . ?

  DynamicSegment # Variable starting with *

    def match_extraction(next_capture)
      # All non code-related keys (such as :id, :slug) are URI-unescaped as
      # path parameters.
      default_value = default ? default.inspect : nil
      %[
        value = if (m = match[#{next_capture}])
          URI.unescape(m)
        else
          #{default_value}
        end
        params[:#{key}] = value if value
      ]
    end

    def regexp_chunk
      if regexp
        if regexp_has_modifiers?
          "(#{regexp.to_s})"
        else
          "(#{regexp.source})"
        end
      else
        "([^#{Routing::SEPARATORS.join}]+)"
      end
    end

    ControllerSegment # :controller

      def match_extraction(next_capture)
        if default
          "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'"
        else
          "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]"
        end
      end

      def regexp_chunk
        possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name }
        "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
      end

    PathSegment # Route globbing segment starting with *

      def match_extraction(next_capture)
        "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}"
      end

      def regexp_chunk
        regexp || "(.*)"
      end

Obviously, the amount of work done by these methods varies depending on which type of Segment they're dealing with. A DividerSegment, for example, will end up not generating any regular expression chunk at all - which means that it will end up being removed from the results of recognition_extraction by the final compact step.

3.1.3. An Extraction Example

Consider a (rather contrived) route such as

map.connect 'misc/:controller/:id/*other', :action => 'show'

When Rails breaks this route up, it comes up with this list of segments:

/            DividerSegment
misc         StaticSegment
/            DividerSegment
:controller  ControllerSegment
/            DividerSegment
:id          DynamicSegment
/            DividerSegment
*other       PathSegment
/            DividerSegment

As recognition_extraction processes this array, it builds up a new array of extraction code. For the first three segments, match_extraction returns nil, so the first three members of the new array are nil. The method also looks at the regular expression chunks that will be built for each segment, to determine which match numbers will be used up by the segment being processed. In the case of DividerSegment and StaticSegment, the regular expressions have no variable matching, so the counter remains unchanged at its initial value of 1.

Things start to get more interesting with the ControllerSegment. Here, match_extraction looks at whether or not the :controller key has a corresponding default. In this example, there is no default, so the generated extraction code looks like this:

params[:controller] = match[1].downcase if match[1]

The corresponding regular expression chunk will return a single match, so the counter gets bumped to 2. Next, the DividerSegment puts another nil into the array. This is followed by a DynamicSegment, which generates a more complex extraction chunk:

value = if (m = match[2])
  URI.unescape(m)
else

end
params[:id] = value if value

There are a few things to note here. First, the new match number is being used to capture the value of the corresponding part of the incoming URI. Second, this is the code that handles the unescaping of strings passed in the URI - for example, turning "my%20photo" back into "my photo". The empty else clause is where match_extraction would put the default value for this segment, if there was one. Finally, this code is the bit that fills in the params array for the parameter in question.

After another DividerSegment, there's a PathSegment to process. Here, the extraction code has to handle splitting the remainder of the URL:

params[:other] = PathSegment::Result.new_escaped((match[3] || "").split('/'))

The end result of recognition_extraction is to generate an array of code snippets, each of which is responsible for extracting one parameter. The write_recognition! method uses this array to build the entire extraction code block through the * method:

recognition_extraction * "\n"
Tip Multiplying an array by a string is equivalent to arr.join(str).

In this example, the final array of extraction code snippets is:

[
 nil,
 nil,
 nil, "params[:controller] = match[1].downcase if match[1]",
 nil,
 "\n          value = if (m = match[2])\n            URI.unescape(m)\n          else\n            \n          end\n          params[:id] = value if value\n        ",
 nil,
 "params[:other] = PathSegment::Result.new_escaped((match[3] || \"\").split('/'))",
 nil
]

Which leads to this code block being generated in write_recognition!

params[:controller] = match[1].downcase if match[1]
          value = if (m = match[2])
            URI.unescape(m)
          else

          end
          params[:id] = value if value

params[:other] = PathSegment::Result.new_escaped((match[3] || \"\").split('/'))

3.1.4. recognition_conditions.

The recognition_conditions method is the start of the code that actually generates the pattern that will match a route:

def recognition_conditions
  result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
  result << "[conditions[:method]].flatten.include?(env[:method])" if conditions[:method]
  result
end

Look first at the call the recognition_pattern. This method builds the regular expression to match the route:

def recognition_pattern(wrap = true)
  pattern = ''
  segments.reverse_each do |segment|
    pattern = segment.build_pattern pattern
  end
  wrap ? ("\\A" + pattern + "\\Z") : pattern
end

The regular expression is built starting with the last segment. That's because of the effect that optional segments can have on the overall regular expression. If a segment is optional, then it has to "wrap" the following parts of the pattern, allowing them to be supplied or omitted. Otherwise, the regular expression for the segment being processed can be prepended to the regular expression that already exists for the following segments.

Starting with the last segment in the route and an empty pattern, this method repeatedly calls the build_pattern method of the segment, passing it the pattern to that point. The build_pattern method is implemented separately for dynamic and static segments. For a static segment, it looks like this:

def build_pattern(pattern)
  escaped = Regexp.escape(value)
  if optional? && ! pattern.empty?
    "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})"
  elsif optional?
    Regexp.optionalize escaped
  else
    escaped + pattern
  end
end

And here's the implementation of build_pattern for a dynamic segment:

def build_pattern(pattern)
  pattern = "#{regexp_chunk}#{pattern}"
  optional? ? Regexp.optionalize(pattern) : pattern
end

TODO: Probably need to go at least one level deeper into the code here.

When it's all done getting back the pattern from the individual segments, recognition_pattern wraps it in "start of string" and "end of string" matchers.

3.1.5. Putting the Pieces Together

With all those pieces taken care of, it's time for a second look at write_recognition!:

# Write and compile a +recognize+ method for this Route.
def write_recognition!
  # Create an if structure to extract the params from a match if it occurs.
  body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
  body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"

  # Build the method declaration and compile it
  method_decl = "def recognize(path, env = {})\n#{body}\nend"
  instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
  method_decl
end

Pay close attention to the way that the body of the generated recognize method is created (because it's not done simply top to bottom). First, the code to extract recognized values from the URL is built, by consulting recognition_extraction (and including a call to parameter_shell in the generated code). Next, this is wrapped up in the conditional call depending on recognition_conditions to put the actual regular expression matching around the extraction code. Finally, the whole is wrapped up in the def recognize shell.

Finally, the generated code is passed off to instance_eval. Though this is part of core ruby, you may not have used it before. instance_eval takes a string and evaluates that string in the context of the current instance - that is, whatever self equals when it is evaluated. If the string contains a method definition, you end up with a new method of the instance (not of the class) - in this case, the particular route object gets equipped with a recognize method. The second parameter to instance_eval provides a template for the information that this method should show in the case of a backtrace.

3.2. How Recognizers are Used

Now you know where the nuts and bolts of the route recognition code come from: each route gets a generated recognize method when it is built from the routing DSL, thanks to the freeze method. But where is this recognize method actually used? To answer that question, you need to look at the Rails dispatching code, which starts off in railties/lib/dispatcher.rb. This file simply creates an ActionController::Dispatcher object for future use:

require 'action_controller/dispatcher'
Dispatcher = ActionController::Dispatcher

But how does anything happen with this Dispatcher class? That starts out in railties/lib/initializer.rb

def prepare_dispatcher
  return unless configuration.frameworks.include?(:action_controller)
  require 'dispatcher' unless defined?(::Dispatcher)
  Dispatcher.define_dispatcher_callbacks(configuration.cache_classes)
  Dispatcher.new(Rails.logger).send :run_callbacks, :prepare_dispatch
end

The set of callbacks in dispatching is fairly involved, and it depends on whether the code is running in development mode or not. But ultimately, incoming requests to your Rails application end up in the handle_request method of ActionController::Dispatcher:

def handle_request
  @controller = Routing::Routes.recognize(@request)
  @controller.process(@request, @response).out(@output)
end

The Routes.recognize method calls recognize_path with the path from the incoming request plus information extracted from the environment:

def recognize(request)
  params = recognize_path(request.path, extract_request_environment(request))
  request.path_parameters = params.with_indifferent_access
  "#{params[:controller].camelize}Controller".constantize
end

By default, extract_request_environment grabs the method from the incoming request:

def extract_request_environment(request)
  { :method => request.method }
end

The recognize_path method is another of the tricky ones. It used to be defined along with the rest of the RouteSet class in route_set.rb, but now it's in recognition_optimisation.rb The optimization code is rather tricky, but the idea is simple: given an ordered list of routes, you don't necessarily have to compare the path with every route to find the first one that matches. Instead, you can skip whole groups of routes. For a simple example, suppose your routes.rb starts out this way:

ActionController::Routing::Routes.draw do |map|
  map.resources :books
  map.resources :photos

That will generate a RouteSet containing 14 routes (two each for each of the seven default RESTful actions, one with the format and one without). Suppose a request comes in for /photos/new. With the old, unoptimized routing code, this request would first have been compared against the seven routes for books. With the current, optimized code, the first comparison will compare the first route segment to "books", and then skip all of the books routes. I'll go into more detail on this process shortly.

Ultimately, some route will recognize the path, and return the parameters hash for that route. Rails saves this hash away in the path_parameters for the request, and then uses the :controller parameter from the route to calculate the name of the Controller class that will handle this route.

So, handling an incoming request is a two-part process. First, the code goes through all of the route instances - which represent the routes, in the order that they were defined in your routes.rb file - until it finds one that matches. The end result of this will be the name of the controller to call. Rails then turns around and hands the request off to that controller to process.

3.3. Optimized Recognition

Although the concept of routing optimization is fairly simple, the code in recognition_optimisation.rb is some of the trickiest in the whole routing process. But if you really want to understand the routing process, you have to dig into it. The entry point to this code is the recognize_path method, which is called when there is a URL that needs to be matched up to a route. This method immediately passes the supplied path and environment variables to recognize_optimized.

The first thing to realize is that the version of recognize_optimized that you see in the file is not the version that runs in your Rails application - at least, not after the first request. The first request for a route does run the original code:

def recognize_optimized(path, environment)
  write_recognize_optimized!
  recognize_optimized(path, environment)
end

The fact that the original version of recognize_optimized calls itself does not indicate recursion. In fact, it would be more accurate to say that it calls another method of the same name. The trick is in write_recognize_optimized!:

def write_recognize_optimized!
  tree = segment_tree(routes)
  body = generate_code(tree)

  remove_recognize_optimized!

  instance_eval %{
    def recognize_optimized(path, env)
      segments = to_plain_segments(path)
      index = #{body}
      return nil unless index
      while index < routes.size
        result = routes[index].recognize(path, env) and return result
        index += 1
      end
      nil
    end
  }, '(recognize_optimized)', 1
end

There are four things going on here:

  1. Generate a segment tree from the full set of routes

  2. Generate some code from that tree

  3. Remove the original definition of recognize_optimized

  4. Use instance_eval to install the generated code as a new version

3.3.1. Building the Segment Tree

The segment tree data structure is built by the segment_tree method, which processes the original set of routes into something that will be better for quickly finding a matching route.

def segment_tree(routes)
  tree = [0]

  i = -1
  routes.each do |route|
    i += 1
    # not fast, but runs only once
    segments = to_plain_segments(route.segments.inject("") { |str,s| str << s.to_s })

    node  = tree
    segments.each do |seg|
      seg = :dynamic if seg && seg[0] == ?:
      node << [seg, [i]] if node.empty? || node[node.size - 1][0] != seg
      node = node[node.size - 1][1]
    end
  end
  tree
end

After setting up an initial empty tree (as you'll see, the tree is represented by a set of nested arrays), this method first runs each route through to_plain_segments. This method simplifies the route by getting rid of all of the DividerSegment segments and tacks a nil on the end. So, for example, if the original route segments look like this:

DividerSegment     /
StaticSegment      books
DividerSegment     /
DynamicSegment     :id
DividerSegment     /
StaticSegment      edit
DividerSegment     /

Then the version of segments used by the tree-building code will look like this:

books
:id
edit
nil

Notice that the code also converts the array of Route objects to an array of strings (plus one nil); since all the recognition code needs to do is compare string values, there's no point in dragging along the extra baggage.

The next step is to go through the generated segment array and position it on the tree. There's a further optimization here: if a segment begins with a colon, we don't really care what the rest of the segment is for comparison purposes, because it can match anything. So any dynamic segment in the array gets replaced with the special symbol :dynamic at this stage in the process.

Building the tree is a matter of sticking arrays of the form [seg, [i]] into the tree, containing the text of the current segment and the index of the route that contains that segment. The location where these arrays are placed inside the larger tree determines the structure of the array. Essentially, the algorithm is to insert the array at the current location if there isn't any content there, or if the content doesn't match the segment, and then move one level deeper into the tree.

That likely won't make any sense on first (or even latter) readings; to get a feel for how this works, you really need to work through an example. So, consider an application with this simple set of routes:

map.resources :books, :photos
map.root :controller => "pages"

This will actually generate 30 routes - 14 for each of the resources (as discussed earlier), plus two for the map.root call (remember, every named route creation gives you both formatted and unformatted versions). But the routing code can make a fair number of simplifications as it builds the search tree.

The initial state of the tree is simple [0]. The first route to be considered (for which i == 0) is the one that maps to the index action in the books controller. Its plain segment array is ["books", nil]. The node the code is working with isn't empty (it contains 0), but the first segment in the array doesn't match the content of the node, so the code appends ["books", [0]] to the array at this point, giving [0, ["books", [0]]]. The current node is then moved to point to the node that the code just inserted (["books", [0]] ). It then looks at the second member of the plain segment array - nil - and determines that it doesn't match "books", so once again, a new node gets appended. The state of the tree after this first route gets processed is then:

[0, ["books", [0, [nil, [0]]]]]

The second route in the routing table is the route for index with a format. This has exactly the same plain segment representation (["books", nil]) as the first route, because to_plain_segments strips off formats. What happens with this route? The node variable gets reset to point to the entire tree, and then the first segment in the plain segments array is compared to the first member of the last subtree of the tree. In this case they match ("books" == "books"), so nothing gets inserted. The node pointer is still bumped down one level, and when the second segment is compared, once again, there is nothing to insert (nil == nil). So after processing the second route, the tree is still in the same state as it was after processing the first route.

The third route in the routing table corresponds to the create action of the books controller. Because this only differs in HTTP verb from the first route (it's a POST instead of a GET), it has the same plain segments representation. Once again, nothing gets added to the tree for this route, or for the fourth route, which is the formatted create route.

The fifth route is the route to create a new book, and this time it has a different plain segments array: ["books", "new", nil]. The first segment still matches the first member of the last subtree, but this time, the next segment is distinct - causing the node ["new", 4] to be inserted, followed by a node representing the nil in the plain segments array. When the code is done adding this route to the tree, the tree looks like this:

[0, ["books", [0, [nil, [0]], ["new", [4, [nil, [4]]]]]]]

When it comes to the route for the edit action, there's another change to recognize. This time the segments array has a dynamic member, so the tree ends up looking like this:

[0, ["books", [0, [nil, [0]], ["new", [4, [nil, [4]]]], [:dynamic, [6, ["edit", [6, [nil, [6]]]]]]]]]

And so it goes, through the rest of the routes for books. What happens when we get to the first route for photos? The first member of the plain segment array (["photos", nil]) doesn't match the first member of the last subtree, so this route gets inserted as a sibling node. The tree looks like this at this point:

[0, ["books", [0, [nil, [0]], ["new", [4, [nil, [4]]]], [:dynamic, [6, ["edit", [6, [nil, [6]]]], [nil, [8]]]]]], ["photos", [14, [nil, [14]]]]]

Ultimately, the routing tree for the entire set of routes in this tiny example looks like this:

[0, ["books", [0, [nil, [0]], ["new", [4, [nil, [4]]]], [:dynamic, [6, ["edit", [6, [nil, [6]]]], [nil, [8]]]]]], ["photos", [14, [nil, [14]], ["new", [18, [nil, [18]]]], [:dynamic, [20, ["edit", [20, [nil, [20]]]], [nil, [22]]]]]], [nil, [28]]]

That's not a terribly useful representation for human beings, but a little reformatting will make it easier to see the tree structure here:

[0,
  ["books",
    [0,
      [nil, [0]],
    ["new",
      [4,
        [nil, [4]]
      ]
    ],
    [:dynamic,
      [6,
        ["edit",
          [6,
            [nil, [6]]
          ]
        ],
        [nil, [8]]
      ]
    ]
  ]],
  ["photos",
    [14,
      [nil, [14]],
    ["new",
      [18,
        [nil, [18]]
      ]
    ],
    [:dynamic,
      [20,
        ["edit",
          [20,
            [nil, [20]]
          ]
        ],
        [nil, [22]]
      ]
    ]
  ]],
  [nil, [28]]
]

This tree gives us an idea of how the optimized recognition code will be structured. For example, if a URI starting with /photos comes in to be recognized, the tree tells us that we can skip down to at least segment 14 before taking a closer look. That's 14 routes (all of the routes for the books resource) that don't have to be tested - and 14 complex regular expressions that our server doesn't have to evaluate.

3.3.2. Generating the Optimization Code

Of course, even though you can look at the array representation of the tree and figure out what to do with a particular URL, ruby isn't quite that smart. The job of the generate_code method is to convert the tree into a series of nested if statements that perform the actual tests. Here's the method that does this work:

def generate_code(list, padding='  ', level = 0)
  # a digit
  return padding + "#{list[0]}\n" if list.size == 1 && !(Array === list[0])

  body = padding + "(seg = segments[#{level}]; \n"

  i = 0
  was_nil = false
  list.each do |item|
    if Array === item
      i += 1
      start = (i == 1)
      final = (i == list.size)
      tag, sub = item
      if tag == :dynamic
        body += padding + "#{start ? 'if' : 'elsif'} true\n"
        body += generate_code(sub, padding + "  ", level + 1)
        break
      elsif tag == nil && !was_nil
        was_nil = true
        body += padding + "#{start ? 'if' : 'elsif'} seg.nil?\n"
        body += generate_code(sub, padding + "  ", level + 1)
      else
        body += padding + "#{start ? 'if' : 'elsif'} seg == '#{tag}'\n"
        body += generate_code(sub, padding + "  ", level + 1)
      end
    end
  end
  body += padding + "else\n"
  body += padding + "  #{list[0]}\n"
  body += padding + "end)\n"
  body
end

The generate_code method walks the generated tree recursively, unwinding it into a linear series of checks. What this method returns depends on where it is in the tree:

  • For a leaf node (which will be an array containing a single number) it returns the number

  • For a node tagged as :dynamic, it inserts an elsif true clause, because a dynamic node will match anything

  • For a nil node, it inserts a test for seg.nil?

  • For any other node, it inserts a test for the value of the segment

The end result is a block of tests that will return a number. That number is the point in the overall Routes array where Rails needs to start looking at routes in more detail to figure out which one matches the incoming URI. In the case of the simple set of routes that I just examined in building the tree, the corresponding block of tests looks like this:

(seg = segments[0];
if seg == 'books'
  (seg = segments[1];
  if seg.nil?
    0
  elsif seg == 'new'
    (seg = segments[2];
    if seg.nil?
      4
    else
      4
    end)
  elsif true
    (seg = segments[2];
    if seg == 'edit'
      (seg = segments[3];
      if seg.nil?
        6
      else
        6
      end)
    elsif seg.nil?
      8
    else
      6
    end)
  else
    0
  end)
elsif seg == 'photos'
  (seg = segments[1];
  if seg.nil?
    14
  elsif seg == 'new'
    (seg = segments[2];
    if seg.nil?
      18
    else
      18
    end)
  elsif true
    (seg = segments[2];
    if seg == 'edit'
      (seg = segments[3];
      if seg.nil?
        20
      else
        20
      end)
    elsif seg.nil?
      22
    else
      20
    end)
  else
    14
  end)
elsif seg.nil?
  28
else
  0
end)

As you can see, the generating code inserts some padding and newlines to make this generated code a bit more readable.

3.3.3. Recognizing the Route

After the big block of conditionals is created, the write_recognize_optimized! method adds the final recognize_optimized method to the Route object. The generated code looks like this:

def recognize_optimized(path, env)
  segments = to_plain_segments(path)
  index = # tree of conditional segments goes here
  return nil unless index
  while index < routes.size
    result = routes[index].recognize(path, env) and return result
    index += 1
  end
  nil
end

When it's passed a route, this code first converts it to a plain segments array. Then it uses that array, together with the generated tree of conditionals, to determine a starting index. Starting at this index, the code iterates through the routes calling the generated recognize method on each one. When it finds a match, it returns the parameters for that route. If it doesn't find a match (because it got to the end of routes), it returns nil to indicate that there is no match.

In the case of no matching route, the recognize_path method of RouteSet tries to raise a helpful error message. First, it tries to recognize the route with every HTTP verb that Rails handles; if any of these succeeds, it will raise a MethodNotAllowed error. If the verb used in the request isn't recognized by Rails, it will raise a NotImplemented error. If all else fails, it raises a generic RoutingError.

TODO: This probably has implications for the best order of calls in routes.rb, but I'm not sure what they are.

4. Generating Routes

To start the tale of how Rails generates a route, given the parameters for that route, go back to the freeze method for a route. Remember, that processing happens during initial route preparation. The part of interest now is write_generation!:

# Write and compile a +generate+ method for this Route.
def write_generation!
  # Build the main body of the generation
  body = "expired = false\n#{generation_extraction}\n#{generation_structure}"

  # If we have conditions that must be tested first, nest the body inside an if
  body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
  args = "options, hash, expire_on = {}"

  # Nest the body inside of a def block, and then compile it.
  raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
  instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"

  # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
  # are the same as the keys that were recalled from the previous request. Thus,
  # we can use the expire_on.keys to determine which keys ought to be used to build
  # the query string. (Never use keys from the recalled request when building the
  # query string.)

  method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
  instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"

  method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
  instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
  raw_method
end

If you followed the discussion of route recognition, this should look somewhat familiar: it's using instance_eval to generate three more methods for each route object as it is set up: generate, generate_raw, and generate_args. It helps to have some idea of what's really being generated here, to help set you up for the discussion to follow. Consider this route:

map.activate '/activate/:id', :controller => 'accounts', :action => 'show'

Here are the methods that will be generated by write_generation!:

def generate_raw(options, hash, expire_on = {})
  path = begin
    if hash[:action] == "show" && hash[:controller] == "accounts"
      expired = false
      id_value = hash[:id] && hash[:id].to_param
      expired, hash = true, options if !expired && expire_on[:id]
      if id_value == nil
        "/activate"
      else
        "/activate/#{URI.escape(id_value.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}"
      end
    end
  end
  [path, hash]
end

def generate(options, hash, expire_on = {})
  path, hash = generate_raw(options, hash, expire_on)
  append_query_string(path, hash, extra_keys(options))
end

def generate_extras(options, hash, expire_on = {})
  path, hash = generate_raw(options, hash, expire_on)
  [path, extra_keys(options)]
end

And just as with recognition, more complex routes will generate more complex generators:

map.connect 'photo/:id/:format', :controller => 'photos', :action => 'show', :defaults => { :format => 'jpg' }, :requirements => { :id => /[A-Z]\d{5}/ }

This route generates these methods:

def generate_raw(options, hash, expire_on = {})
  path = begin
    if hash[:action] == "show" && hash[:controller] == "photos"
      expired = false
      id_value = hash[:id] && hash[:id].to_param
      return [nil,nil] unless id_value && /\A(?-mix:[A-Z]\d{5})\Z/ =~ id_value
      expired, hash = true, options if !expired && expire_on[:id]
      format_value = hash[:format] && hash[:format].to_param || "jpg"
      expired, hash = true, options if !expired && expire_on[:format]
      if format_value == "jpg"
        "/photo/#{URI.escape(id_value.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}"
      else
        "/photo/#{URI.escape(id_value.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}/#{URI.escape(format_value.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}"
      end
    end
  end
  [path, hash]
end

def generate(options, hash, expire_on = {})
  path, hash = generate_raw(options, hash, expire_on)
  append_query_string(path, hash, extra_keys(options))
end

def generate_extras(options, hash, expire_on = {})
  path, hash = generate_raw(options, hash, expire_on)
  [path, extra_keys(options)]
end

If you look at the code for write_generation!, you'll see that generate and generate_extras have fixed bodies; the only reason that they're generated dynamically is to adjust their list of arguments. Those two methods are wrappers around generate_raw, so I'll focus my attention on the latter method. This means digging into generation_extraction, generation_structure, and generation_requirements.

4.1. Extracting Options from the Route Hash with generation_extraction

The generation_extraction method is responsible for building code to extract values from an :options hash. These are the pieces of code that look like this:

id_value = hash[:id] && hash[:id].to_param
return [nil,nil] unless id_value && /\A(?-mix:[A-Z]\d{5})\Z/ =~ id_value
expired, hash = true, options if !expired && expire_on[:id]

It does this by iterating over the segments for the route, calling their extraction_code method:

def generation_extraction
  segments.collect do |segment|
    segment.extraction_code
  end.compact * "\n"
end

As you might expect, extraction_code differs for different types of segments. At the level of the base Segment class, it does nothing:

def extraction_code
  nil
end

But the method gets overridden in DynamicSegment:

def extraction_code
  s = extract_value
  vc = value_check
  s << "\nreturn [nil,nil] unless #{vc}" if vc
  s << "\n#{expiry_statement}"
end

Thus, the overall generation_extraction array will only contain entries for DynamicSegment (and its children, ControllerSegment and PathSegment). Everything else will be nil, and get squished out by the call to compact.

The bits of extraction code that do get generated deserve a bit closer look. They're made up from three pieces: the extract_value, the value_check, and the expiry_statement.

4.1.1. Extracting a Value from the Hash with extract_value

The extract_value method builds the line of code that actually extracts a value from the hash that gets passed into generate_raw. For a simple dynamic segment, the method looks like this:

def extract_value
  "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}"
end

There's a bit more to this than meets the eye. If you are looking at a segment :foo, it will automatically pull the :foo value out of the supplied hash. But then it will invoke to_param if possible (this is how Rails gets friendly URLs into generated routes), and (if the segment has a default), use the default value if no value is supplied in the hash. The local_name method generates the name for the variable that will hold the extracted value; it's set equal to the key for this segment plus the suffix _value.

The implementation of extract_value for a ControllerSegment is somewhat different:

def extract_value
  "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase"
end

The method can be simpler for controller segments because they won't have a to_param to invoke. On the other hand, a PathSegment has to use a more complex implementation of this method:

def extract_value
  "#{local_name} = hash[:#{key}] && Array(hash[:#{key}]).collect { |path_component|
        URI.escape(path_component.to_param, ActionController::Routing::Segment::UNSAFE_PCHAR)
        }.to_param #{"|| #{default.inspect}" if default}"
end

4.1.2. Handling Requirements with value_check

The value_check method builds the line of code that checks the requirements for this segment (if any). Here's the implementation for DynamicSegment:

def value_check
  if default # Then we know it won't be nil
    "#{value_regexp.inspect} =~ #{local_name}" if regexp
  elsif optional?
    # If we have a regexp check that the value is not given, or that it matches.
    # If we have no regexp, return nil since we do not require a condition.
    "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp
  else # Then it must be present, and if we have a regexp, it must match too.
    "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}"
  end
end

There are several different possible checks, depending on whether a particular segment has a default or if it's optional.

4.2. Parameter Expiration with expiry_statement

The final step of the generation_extraction process is to call expiry_statement. This method is responsible for generating a line of code to handle parameter expiration for this segment. The implementation is straightforward:

def expiry_statement
  "expired, hash = true, options if !expired && expire_on[:#{key}]"
end

The only thing that changes in the generated code is the key that the conditional is inspecting. This line of code is used to implement parameter expiration: if the route generation code determines that the current key is expired, then it switches the route generation process from using the recalled parameters of the current request (passed in as the hash hash) to using the new parameters in the options hash.

What's less straightforward is the entire concept of parameter expiration. You'll find a section explaining this idea later in this guide.

4.3. Building the Path with generation_structure

The write_generation! method next calls generation_structure, whose job it is to build the code that combines segments together to construct a path. This method looks deceptively simple at first glance:

def generation_structure
  segments.last.string_structure segments[0..-2]
end

So, this apparently starts with the last segment in the route, and calls its string_structure method, passing in an array of all of the preceding segments in the route. The string_structure method is another one that changes depending on which type of segment you're dealing with. For the base Segment class, it's defined this way:

def string_structure(prior_segments)
  optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments)
end

But for DynamicSegment (and its descendants) the method is different:

def string_structure(prior_segments)
  if optional? # We have a conditional to do...
    # If we should not appear in the url, just write the code for the prior
    # segments. This occurs if our value is the default value, or, if we are
    # optional, if we have nil as our value.
    "if #{local_name} == #{default.inspect}\n" +
      continue_string_structure(prior_segments) +
    "\nelse\n" + # Otherwise, write the code up to here
      "#{interpolation_statement(prior_segments)}\nend"
  else
    interpolation_statement(prior_segments)
  end
end

To see what this does, I'll take a very simple route:

map.activate '/activate/:id', :controller => 'accounts', :action => 'show'

This route will generate 5 segments when it's first processed:

  1. / (DividerSegment)

  2. activate (StaticSegment)

  3. / (DividerSegment)

  4. :id (DynamicSegment)

  5. / (DividerSegment)

The generation_structure method will start by calling string_structure on the last segment, passing in all of the previous segments as an array. This will end up in the base implementation of string_structure, since DividerSegment is not a descendant of DynamicSegment. And because a DividerSegment is always optional, it will just call continue_string_structure with that array of previous segments. The job of continue_string_structure is to take us one more segment into the process:

def continue_string_structure(prior_segments)
  if prior_segments.empty?
    interpolation_statement(prior_segments)
  else
    new_priors = prior_segments[0..-2]
    prior_segments.last.string_structure(new_priors)
  end
end

In the case I'm tracing, the prior_segments array is not empty, so the code peels off the new last segment (the :id dynamic segment) and feeds it, along with the remaining three segments, back into string_structure. This time, though, it ends up in the DynamicSegment implementation. The :id segment is not optional, so the code ends up calling interpolation_statement(prior_segments):

# Return a string interpolation statement for this segment and those before it.
def interpolation_statement(prior_segments)
  chunks = prior_segments.collect { |s| s.interpolation_chunk }
  chunks << interpolation_chunk
  "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}"
end

The job of interpolation_segment is to build the actual string interpolation statement that will be used to generate the path. It does this by collecting interpolation "chunks" from all prior segments using interpolation_chunk, and then tacking on the interpolation_chunk for the current segment. The interpolation_chunk method has several implementations. On the base Segment class:

def interpolation_chunk
  URI.escape(value, UNSAFE_PCHAR)
end

On the StaticSegment class:

def interpolation_chunk
  raw? ? value : super
end
Note A DividerSegment sets raw to true on creation.

On the DynamicSegment class:

def interpolation_chunk(value_code = local_name)
  "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}"
end

On the ControllerSegment class:

# Don't URI.escape the controller name since it may contain slashes.
def interpolation_chunk(value_code = local_name)
  "\#{#{value_code}.to_s}"
end

On the PathSegment class:

def interpolation_chunk(value_code = local_name)
  "\#{#{value_code}}"
end

So, having these bits at our fingers, here's what gets returned by generation_structure for the segment that we're looking at:

  1. / (DividerSegment) returns "/"

  2. activate (StaticSegment) returns "activate"

  3. / (DividerSegment) returns "/"

  4. :id (DynamicSegment) returns "#{URI.escape(id_value.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}"

And the final generated structure is:

"/activate/#{URI.escape(id_value.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}"

That's the end of this example, though not quite the end of generation_structure. The last bit of interpolation_statement is a call to all_optionals_available_condition with the prior segment array. The job of this method is to build an if statement that prevents the path from being built if any preceding optional segments are missing from the provided hash.

4.3.1. Ensuring Requirements with generation_requirements

The last major piece called by write_generation! is generation_requirements. This piece ensures that we don't do a lot of extra work if the requirements for a route are not met. Because the route maintains its own list of requirements, this one doesn't need to dive into the segments collection:

def generation_requirements
  requirement_conditions = requirements.collect do |key, req|
    if req.is_a? Regexp
      value_regexp = Regexp.new "\\A#{req.to_s}\\Z"
      "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
    else
      "hash[:#{key}] == #{req.inspect}"
    end
  end
  requirement_conditions * ' && ' unless requirement_conditions.empty?
end

This code walks through the full hash of requirements, building a separate conditional check for each one based on whether it is a regular expression requirement or a literal requirement. Then it uses the && operator to stick them all together.

4.4. Putting the Pieces Together

Now that you've seen the pieces, it's time to review how they fit together. Here's the write_generation! method that gets called when a route is processed again:

# Write and compile a +generate+ method for this Route.
def write_generation!
  # Build the main body of the generation
  body = "expired = false\n#{generation_extraction}\n#{generation_structure}"

  # If we have conditions that must be tested first, nest the body inside an if
  body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
  args = "options, hash, expire_on = {}"

  # Nest the body inside of a def block, and then compile it.
  raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
  instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"

  # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
  # are the same as the keys that were recalled from the previous request. Thus,
  # we can use the expire_on.keys to determine which keys ought to be used to build
  # the query string. (Never use keys from the recalled request when building the
  # query string.)

  method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
  instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"

  method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
  instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
  raw_method
end

The code first builds up the method body from the generation_extraction and generation_structure pieces. If there are requirements, then generation_requirements is used to wrap this body. It's then wrapped further in the code to define a method and return the desired values, and handed off to instance_eval to attach as a method of the route.

4.5. Parameter Expiration

Everything that Rails does is in response to an incoming request. If you're generating routes, you're still in the context of a request. The task of parameter expiration is to make route generation aware of the current request so that it can be smart about the routes that it generates. For example, if you're in the middle of a request for a page whose parameter hash is { :controller ⇒ "photos", :action ⇒ "show", :id ⇒ "17" } and you ask for a link to { :action ⇒ :edit, :id ⇒ 22 }, Rails is smart enough to re-use the :controller key from the current request, but not the :action or :id keys. It does this by marking those keys as expired in the expiry hash. The expiry hash contains the keys for the current request, with keys that are reusable in generation (i.e., not expired) hashed to true, and keys that are expired hashed to false. When building a route, Rails will only reuse non-expired keys.

4.6. How Generators are Used

Now you know what a route generator looks like. The other question is how it gets called. In general, route generators are invoked from link_to, url_for, or from calls to specific routes such as new_photo_path or edit_book_url. The different methods take different paths, but eventually you'll end up at RouteSet#generate.

For example, say you include a simple link_to call in a view:

<%= link_to "Photos", { :controller => "photos", :action => "index" } %>

The code to handle this is in /actionpack/lib/action_view/helpers/url_helper.rb. There you will find that link_to just hands off the routing options (in this case, photos_path) to url_for:

name         = args.first
options      = args.second || {}
html_options = args.third

url = url_for(options)

The url_for method, in the same file, looks at what was passed in options to decide where to dispatch the work next:

def url_for(options = {})
  options ||= {}
  url = case options
  when String
    escape = true
    options
  when Hash
    options = { :only_path => options[:host].nil? }.update(options.symbolize_keys)
    escape  = options.key?(:escape) ? options.delete(:escape) : true
    @controller.send(:url_for, options)
  when :back
    escape = false
    @controller.request.env["HTTP_REFERER"] || 'javascript:history.back()'
  else
    escape = false
    polymorphic_path(options)
  end

  escape ? escape_once(url) : url
end

In this case, it's off to ActionController::Base#url_for:

def url_for(options = {})
  options ||= {}
  case options
    when String
      options
    when Hash
      @url.rewrite(rewrite_options(options))
    else
      polymorphic_url(options)
  end
end

We're still passing a hash of options along, so the next stop is the rewrite method of a UrlRewriter instance (the UrlRewriter is set up by initialize_current_url, which is called by the process method that kicks off performing a particular action). This method itself is just a wrapper:

def rewrite(options = {})
  rewrite_url(options)
end

The rewrite_url method does a fair bit of work to make sure that the rewritten URL string has the right protocol, root, anchor, and so on. But for the purposes of route generation, it's the call to generate the path from the options that matters:

path = rewrite_path(options)

Finally, with rewrite_path, we are nearly to the actual route generation code:

def rewrite_path(options)
  options = options.symbolize_keys
  options.update(options[:params].symbolize_keys) if options[:params]

  if (overwrite = options.delete(:overwrite_params))
    options.update(@parameters.symbolize_keys)
    options.update(overwrite.symbolize_keys)
  end

  RESERVED_OPTIONS.each { |k| options.delete(k) }

  # Generates the query string, too
  Routing::Routes.generate(options, @request.symbolized_path_parameters)
end

The rewrite_path method first puts the options into a canonical form, because Rails accepts a variety of syntaxes for specifying options and it's convenient to be dealing with a known format when actually working with them. It then gets rid of a bunch of possible options that are not used by route generation:

RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :skip_relative_url_root]

Finally, rewrite_path sends the options it received off to RouteSet#generate, which, you may recall, is where I said it was headed in the first place. It also sends along a hash of options that correspond to the route for the current request, which, as you'll shortly see, are important inputs to the route generation process.

RouteSet#generate is one of the longer methods in the Rails framework:

def generate(options, recall = {}, method=:generate)
  named_route_name = options.delete(:use_route)
  generate_all = options.delete(:generate_all)
  if named_route_name
    named_route = named_routes[named_route_name]
    options = named_route.parameter_shell.merge(options)
  end

  options = options_as_params(options)
  expire_on = build_expiry(options, recall)

  if options[:controller]
    options[:controller] = options[:controller].to_s
  end
  # if the controller has changed, make sure it changes relative to the
  # current controller module, if any. In other words, if we're currently
  # on admin/get, and the new controller is 'set', the new controller
  # should really be admin/set.
  if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
    old_parts = recall[:controller].split('/')
    new_parts = options[:controller].split('/')
    parts = old_parts[0..-(new_parts.length + 1)] + new_parts
    options[:controller] = parts.join('/')
  end

  # drop the leading '/' on the controller name
  options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/
  merged = recall.merge(options)

  if named_route
    path = named_route.generate(options, merged, expire_on)
    if path.nil?
      raise_named_route_error(options, named_route, named_route_name)
    else
      return path
    end
  else
    merged[:action] ||= 'index'
    options[:action] ||= 'index'

    controller = merged[:controller]
    action = merged[:action]

    raise RoutingError, "Need controller and action!" unless controller && action

    if generate_all
      # Used by caching to expire all paths for a resource
      return routes.collect do |route|
        route.__send__(method, options, merged, expire_on)
      end.compact
    end

    # don't use the recalled keys when determining which routes to check
    routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]

    routes.each do |route|
      results = route.__send__(method, options, merged, expire_on)
      return results if results && (!results.is_a?(Array) || results.first)
    end
  end

  raise RoutingError, "No route matches #{options.inspect}"
end

In most cases, the important inputs to this method are options, which holds the hash of options that you specified to generate a path (things like :id ⇒ 32), and recall, a hash of path parameters for the current request. The recall parameter is used in cases where Rails needs to do "smart" route generation, building routes relative to where you are now in the application.

Much of the code in generate is concerned with setting things up in the options. For example, if you're at an admin/set page, and you ask for a path involving get, the code is smart enough to transform that to admin/get. There's also some code to make sure we don't bother with routes that will clearly not match this request. Ultimately, though, you end up with a routes array of candidate routes, and the code executes the generate method on each of these routes. The first one to return a result is the one that gets used to generate the route.

With that high-level view out of the way, I'll look at the generate method one step at a time.

4.6.1. Setting Initial Options

def generate(options, recall = {}, method=:generate)
  named_route_name = options.delete(:use_route)
  generate_all = options.delete(:generate_all)

The first step is to pull out a pair of special keys from the options hash (if they're included). The :use_route key provides the name of a specific named route to generate (if this key is not present, it means that the code should generate a route by inspecting the rest of the options). The generate_all key appears to be meant to tell Rails to expire all of the segments in a route, but as far as I can tell, generate is never called with this key present in the current routing code.

4.6.2. Special Handling for Named Routes

  if named_route_name
    named_route = named_routes[named_route_name]
    options = named_route.parameter_shell.merge(options)
  end

If the generate method was passed the name of a named route, then it can retrieve that route directly by name from the named_routes collection. With the route in hand, the code uses its parameter shell - a hash of all of the parameter values that are not in the route's path, but that should be in the path - as the basis for updating the options used in route generation. For example, given this route:

map.activate '/activate/:id', :controller => 'accounts', :action => 'show'

The parameter shell for this route is {:controller ⇒ accounts, :action ⇒ show}. The NamedRoute object generates this has by looking at the requirements collection and extracting all of the requirements that are not regular expressions.

4.6.3. Making the Action Explicit

  options = options_as_params(options)

The options_as_params method has two jobs:

  1. If the options hash has a :controller key but not an :action key, then set the :action key to "index".

  2. If the :action key is a symbol, convert it to a string.

Making the :action key explicit handles the case where the current request does not have an :action key but we still have to get the parameter expiration correct.

4.6.4. Building the Expiry Hash

  expire_on = build_expiry(options, recall)

The build_expiry method creates the hash that tracks which keys within the options hash are expired.

 def build_expiry(options, recall)
   recall.inject({}) do |expiry, (key, recalled_value)|
     expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param)
     expiry
   end
 end

The expiry hash has a key for every key in the recall hash (the path options that were generated for the current request). It decides whether a key is expired by looking for the same key in the current options hash. If the key is present, and it has a different value in the options has and the recall hash, then the key in the recall hash is expired and should not be used for route generation.

4.6.5. Figuring out the Right :Controller Key

  if options[:controller]
    options[:controller] = options[:controller].to_s
  end
  # if the controller has changed, make sure it changes relative to the
  # current controller module, if any. In other words, if we're currently
  # on admin/get, and the new controller is 'set', the new controller
  # should really be admin/set.
  if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
    old_parts = recall[:controller].split('/')
    new_parts = options[:controller].split('/')
    parts = old_parts[0..-(new_parts.length + 1)] + new_parts
    options[:controller] = parts.join('/')
  end

  # drop the leading '/' on the controller name
  options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/

The :controller key in the options hash may not be the actual :controller key that should be used in route generation. If the code isn't processing a named route, and the current :controller key does not start with a / character, then pull any leading path segments off of the recalled controller key and prepend them to the supplied :controller key.

4.6.6. Merging Recalled and New Options

  merged = recall.merge(options)

In general, route generation can use options from the current request as well as the actual options that were passed to generate the route. The code created a merged hash holding all of these options, overwriting keys from the recalled hash with keys that are explicitly supplied for this generation.

4.6.7. Generating a Named Route

  if named_route
    path = named_route.generate(options, merged, expire_on)
    if path.nil?
      raise_named_route_error(options, named_route, named_route_name)
    else
      return path
    end
  else
        # ...
  end

If we're trying to generate a named route, things are comparatively simple at this point. We pass in all of the hashes - new options, merged options, and expiry - to the named route's generate method to tell it to generate itself. If all goes well, we'll get back the path we're looking for. If there are any problems (for example, you did not supply a required option), then the code raises an error. Otherwise it just hands the generated path back to the caller.

4.6.8. Generating a Regular Route

  if named_route
        # ...
  else
    merged[:action] ||= 'index'
    options[:action] ||= 'index'

    controller = merged[:controller]
    action = merged[:action]

    raise RoutingError, "Need controller and action!" unless controller && action

    if generate_all
      # Used by caching to expire all paths for a resource
      return routes.collect do |route|
        route.__send__(method, options, merged, expire_on)
      end.compact
    end

    # don't use the recalled keys when determining which routes to check
    routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]

    routes.each do |route|
      results = route.__send__(method, options, merged, expire_on)
      return results if results && (!results.is_a?(Array) || results.first)
    end
  end

The generation process for a regular route is a bit more complex than the process for a named route. Rails first checks to be sure that both :controller and :action keys are present in the merged hash. If not, it can give up right away.

With a regular route, it's possible that several routes will potentially match the passed-in options. The generate method looks in the routes_by_controller hash to get a list of the potential routes for the current options. This hash is keyed by controller, action, and the keys in the options hash. Each member of the routes_by_controller hash is lazy-initialized by a call to the routes_by_controller method:

def routes_by_controller
  @routes_by_controller ||= Hash.new do |controller_hash, controller|
    controller_hash[controller] = Hash.new do |action_hash, action|
      action_hash[action] = Hash.new do |key_hash, keys|
        key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys)
      end
    end
  end
end

The nesting code here just ensures that the generated list of potential routes will be placed in the correct spot in the routes_by_controller hash. The real work of finding potential routes is in routes_for_controller_and_action_and_keys:

def routes_for_controller_and_action_and_keys(controller, action, keys)
  selected = routes.select do |route|
    route.matches_controller_and_action? controller, action
  end
  selected.sort_by do |route|
    (keys - route.significant_keys).length
  end
end

The routes_for_controller_and_action_and_keys looks in the array of all possible routes, and checks matches_controller_and_action? for each route; any route that returns true from this method is a candidate for the route that we're looking for:

def matches_controller_and_action?(controller, action)
  prepare_matching!
  (@controller_requirement.nil? || @controller_requirement === controller) &&
  (@action_requirement.nil? || @action_requirement === action)
end

The prepare_matching! method does lazy initialization to figure out the requirements for the route's controller and action:

def prepare_matching!
  unless defined? @matching_prepared
    @controller_requirement = requirement_for(:controller)
    @action_requirement = requirement_for(:action)
    @matching_prepared = true
  end
end

The requirement_for method looks at the hash of requirements to determine whether it contains an entry for the specified key. If not, it goes through the segments for the route, and returns the regular expression for a segment if the segment uses that key.

With the matching prepared, matches_controller_and_action? determines that a route is a match if the controller and action match the regular expression for a segment of the route (note the use of the RegExp#=== operator), or if they're not required by the route. This allows routes_for_controller_and_action_and_keys to come up with its list of potential routes, which are then sorted by the number of keys they contain; this sorting ensures that generated routes will use as many keys from the supplied options as possible, rather than sticking them in the querystring unnecessarily.

Finally, armed with the array of candidate routes, the generate method attempts to generate a path. It does this by sending the :generate method to each possible route, and it accepts the results from the first one that actually returns a path.

4.6.9. Handling Errors

  raise RoutingError, "No route matches #{options.inspect}"
end

If regular route generation falls off the end of the array of candidate routes, then it just raises an error back to the caller.

5. Routing Extension Points

There are many places where you can extend the routing code with your own plugins. Here are some of the most obvious:

  • Extending Mapper

  • Extending RouteBuilder

  • Extending recognition_conditions

  • Extending extract_request_environment

  • Using route generation for other purposes

Note Of course, if you've come up with a routing extension that would benefit the majority of Rails developers, and you understand routing well enough to make it work quickly and accurately, you should consider submitting it as a patch to the Rails core.

5.1. Extending Mapper

If you want to add to the semantics of map by mapping a new kind of entity, you can install yourself as an extension to Mapper. This is what ActionController::Resources does to create map.resources. At the end of the resources.rb file, you'll find:

class ActionController::Routing::RouteSet::Mapper
  include ActionController::Resources
end

With this inclusion, map.resource and map.resources have entirely new meanings, instead of being routes named resource and resources.

5.2. Extending RouteBuilder

If you want to expand the list of possible segment types in a route, or recognize segments differently than Rails does by default, you should consider subclassing the RouteBuilder class. RouteSet uses a builder method to lazily instantiate the builder that it uses, so you can install your subclass using an overridden RouteSet#builder method.

5.3. Extending recognition_conditions

If you want to change how routes are recognized - for example, to require a request to come from a particular range of IP addresses to be considered valid - a good place to start is with Route#recognition_conditions. Overriding this method will allow you to change the way that Rails writes the regular expression to match an incoming request.

5.4. Extending extract_request_environment

If you need attributes other than the request's method to route requests according to your custom scheme, you'll want to override RouteSet#extract_request_environment. For example, if you wanted your routes to be able to inspect the TLD of the request, this is where you'd pull that information out and stick it in the appropriate hash.

5.5. Using Routes for Other Purposes

If you want to do something with routes other than generate them, you can pass your own custom method name into RouteSet#generate. Ultimately, this method will be sent to the matching route, so you'll need to include the code that you want executed into the Route class. For example, this hook would allow you to build a custom route documenter.

6. Open Issues

Despite my best efforts, there are still some parts of the routing code that are relatively opaque to me. If anyone knows the answers to these questions, I'd love to hear them:

  • Route#matches_controller_and_action? calls prepare_matching!. But we call prepare_matching! from Route#freeze, which is called from RouteBuilder.build. Doesn't this mean that the route will always be prepared before it hits the matches_controller_and_action? call?

  • RouteSet#generate includes code to set default values for action into the merged and options hashes. But isn't this already handled by the call higher up in the same method to options_as_params(options)?

  • RouteSet#generate understands a :generate_all option. There's a comment that this is for caching to expire all the paths for a resource, and a test case, but I don't see anywhere in the Rails source that generate actually gets called with this option. Stale code?

  • There's code in ActionController::Routing::Optimisation that should be documented.

7. Credits

This guide would not have been possible without a three-part series covering routing that was published as part of Jamis Buck's Under the Hood series. Though his explanation of routing is two years old at this point, it was an invaluable starting point in understanding the current code.

8. Changelog

  • November 15, 2008: Updated for changes in 2.2RC2. map_resource_routes could use more detail. by Mike Gunderloy

  • September 26, 2008: Added information on extensibility points and Open Issues section by Mike Gunderloy

  • September 25, 2008: Added details on parameter expiration and finding the candidate routes by Mike Gunderloy

  • September 23, 2008: Added details on how link_to gets to RouteSet#generate by Mike Gunderloy

  • September 22, 2008: Added details on recognition extraction by Mike Gunderloy

  • September 21, 2008: Added details on optimized route recognition, route generation process, and other odds and ends by Mike Gunderloy

  • September 20, 2008: More details on named route generation and install_helpers, filled in various other gaps by Mike Gunderloy

  • September 19, 2008: First draft (many TODOs left) by Mike Gunderloy