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
|
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.
|
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.
|
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:
-
Call clear! to get rid of any existing routes
-
Create a new instance of ActionController::Routing::RouteSet::Mapper, and yield it to the block
-
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
|
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:
-
Create an instance of the Resource class
-
Use map_collection_actions to set up collection routes
-
Use map_default_collection_actions to set up default collection routes
-
Use map_new_actions to set up new routes
-
Use map_member_actions to set up member routes
-
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:
-
Create an instance of the SingletonResource class
-
Use map_collection_actions to set up collection routes
-
Use map_default_singleton_actions to set up default singleton routes
-
Use map_new_actions to set up new routes
-
Use map_member_actions to set up member routes
-
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.
|
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:
-
Use regular expressions to determine if the supplied path is a match for the route
-
If so, process the contents of the route and return the route parameters
-
Otherwise, don't return anything
|
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: