Under the hood: Rails' routing DSL
Rails is chock full of magic. From ActiveRecord to ActionView, the full stack employs just about every Ruby idiom in the book to make the programming experience as smooth, painless, and seamless as possible. This comes at a price, though: the source code is generally pretty opaque to Ruby novices. We’ve done our best to keep things readable (it’s in our best interest, after all, since the easier it is to read, the easier it is for people to create and submit patches), but there are still certain areas of Rails that are widely regarded as “hard to follow”.
Routing is one of those areas.
This is the first of three articles that will delve into the dark recesses of the routing code. It deals only with the implementation of the routing DSL (e.g., the part you use in config/routes.rb
). The next two articles will deal with route recognition, and route generation, respectively.
I’d encourage you to follow along in the routing code. This article covers the version of routing in edge rails. You’ll find that it bears very little relation to the version of routing in Rails 1.1.6 and earlier, but don’t let that throw you. As of this writing, revision 5169 contains the latest version of routing.rb
.
The config/routes.rb
file forms the primary (and generally, only) interface to routing for most Rails developers. If you look in the config/routes.rb
file for the vast majority of Rails applications, you’ll see something like this:
1 2 3 4 |
ActionController::Routing::Routes.draw do |map| # ... map.connect ":controller/:action/:id" end |
That ActionController::Routing::Routes
reference is rather misleading. It is not a class, but is simply a constant referring to an instance of ActionController::Routing::RouteSet
. (You can see the instantiation occurring at the bottom of action_controller/routing.rb
.) That one instance is in charge of managing the routes for the entire lifetime of the Ruby process.
1 2 3 4 5 6 |
module ActionController module Routing # ... Routes = RouteSet.new end end |
The RouteSet#draw
method is where the DSL magic all begins. If you look in the routing.rb
code (around line 1113), it’s only three lines long, but what a significant three lines those are!
1 2 3 4 5 |
def draw clear! yield Mapper.new(self) named_routes.install end |
That one method is the entry point for the entire routing DSL. First, any existing routes are removed from the collection (via the clear!
method). Then, a new ActionController::Routing::RouteSet::Mapper
instance is created and yielded to the block (where it is generally bound to the map
variable in config/routes.rb
). After all the routes have been “drawn” (or defined), any named routes are installed into ActionController::Base, so that the named routing helpers can be used by controller and view code (as foo_url
and foo_path
).
ActionController::Routing::RouteSet::Mapper
is nearly trivial. It’s just a proxy that delegates to the RouteSet
instance that created it. When you call map.connect
, the work is delegated to RouteSet#add_route
. When you create a named route, the method_missing
hook on the Mapper
redirects the call to RouteSet#add_named_route
. If you are looking for ways to extend routing, take note: Mapper
is what you need to extend in order to add to or change the routing DSL. For an example of how to extend it, take a look at action_controller/resources.rb. (That’s where the new RESTful routing options are defined.)
So, the call chain so far goes something like RouteSet#draw
, Mapper#connect
, RouteSet#add_route
. Looking at RouteSet#add_route
(line 1147), you’ll see it’s another tiny method of only three lines. Instead of doing all the work itself, it just calls on builder.build
to create the new Route
instance, and then adds the new route to the routes
collection.
1 2 3 4 5 |
def add_route(path, options = {}) route = builder.build(path, options) routes << route route end |
Looking at the definition for the builder
method (line 1109), it just lazily instantiates an ActionController::Routing::RouteBuilder
object and returns it. RouteBuilder
is a factory class for creating new Route
instances. You’ll find it defined beginning at line 785.
1 2 3 |
def builder @builder ||= RouteBuilder.new end |
The RouteBuilder
is another good class to note if you are trying to extend Rails. The fact that RouteSet
uses a builder
method to lazily instantiate the builder means you can easily subclass RouteBuilder
and then install your subclass using an overridden RouteSet#builder
method. (You might want do this if, for instance, your routing extension adds a new kind of routing segment that needs special consideration during parsing.)
The main job of RouteBuilder
is to take a path string, and a hash of options, and return a Route that corresponds to them. It does this by calling segments_for_route_path
(line 807) to decompose (or tokenize) the path into “segments”. (Segments are the atomic substrings of the path, which represent the delimiters, static text, and dynamic tokens it contains.) The builder then calls assign_route_options
to combine the default values and condtions with those segments. Sounds complex, but it’s actually remarkably straightforward.
You can see that segments_for_route_path
just calls segment_for
repeatedly to decompose the string. Each call to segment_for
returns a new Segment
instance that represents some section of the string.
Looking at segment_for
, you can see that it just uses a case statement with regexen to determine what type of segment to return:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def segment_for(string) segment = case string when /\A:(\w+)/ key = $1.to_sym case key when :controller then ControllerSegment.new(key) else DynamicSegment.new key end when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true) when /\A\?(.*?)\?/ returning segment = StaticSegment.new($1) do segment.is_optional = true end when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1) when Regexp.new(separator_pattern) then returning segment = DividerSegment.new($&) do segment.is_optional = (optional_separators.include? $&) end end [segment, $~.post_match] end |
(That returning
statement may look odd; it’s a method defined in ActiveSupport that makes it really easy to return a value, but only after performing some operations on it. You’ll find it used all over in Rails, so it’s worth getting familiar with it.)
If you are trying to extend Routes, this is where your RouteBuilder
subclass would extend segment_for
to add it’s own custom string processing. As you can see, routing currently supports five different segment types:
-
DynamicSegment
. This represents parts of the route that begin with a colon, like:action
,:permalink
or:id
. -
ControllerSegment
. This is actually a subclass ofDynamicSegment
. It represents to special string:controller
, because it does some special recognition on those strings. (We’ll cover that more in the next article). -
PathSegment
. This is for segments that start with an asterisk, and which represent the remainder of the path. Routes like"/file/*path"
use aPathSegment
. -
StaticSegment
. This is any static text in your route that must be matched (or generated) verbatim. If you have a path like"/one/two"
, the strings"one"
and"two"
are both static segments. -
DividerSegment
. This is any segment that is used to delimit the other segments. Generally, this will be the forward slash character, but also includes commas, periods, semicolons, and question marks.
The new Route, once instantiated, will include an array of segments that encode the path it encapsulates. You can inspect the route by calling it’s #to_s
method. That will give you a readable version of its path, and its options, should you ever need it. In fact, when I’m troubleshooting something, I find it helpful to add something like the following to the end of config/routes.rb
:
1 2 3 |
ActionController::Routing::Routes.routes.each do |route| puts route end |
Rick Olson has also created a routing navigator plugin for Rails that makes it easy to see what routes exist in your project. This is especially handy if you are using the RESTful routes, since they dynamically generate a whole host of routes behind the scenes.
Before we conclude this whirlwind tour, we need to make one last brief stop. When you create a named route, the mapper delegates to the RouteSet#add_named_route
method, which (after calling add_route
) delegates to a helper object called named_routes
. Each RouteSet
instance includes a reference to a NamedRouteCollection
instance (see line 986), called named_routes
. Any time you add a route to this collection, a set of helper methods are automatically generated for that route and are added to an anonymous module, which is used to install the helpers into (e.g.) the ActionController::Base class. This installation only occurs when the NamedRouteCollection#install
method is called, and that happens at the end of RouteSet#draw
.
If you have any questions about something I glossed over (or omitted), please feel free to ask in the comments and I’ll try to answer.
That, then, is the overview of the implementation of the Routing DSL. The next article will deal with how this all ties into route recognition, which comes into play every time a request is received by a Rails application. Stay tuned!
Reader Comments
2 Oct 2006
2 Oct 2006
2 Oct 2006
2 Oct 2006
2 Oct 2006
2 Oct 2006
2 Oct 2006
3 Oct 2006
3 Oct 2006
3 Oct 2006
3 Oct 2006
Okay. Maybe I’m getting ahead and want to hear more about recognition, but this has a specification flavor as well. One thing that mystifies me is the grouped-controller routes, as when the Foo::BarController lives under app/controllers/foo/bars_controller.rb
While the standard
does the appropriate recognition (:controller gets ‘foo/bars’) I’ve got some ajax that needs specialized route processing, like :controller/:action/:target/:value. Unfortunately, the route specification
doesn’t work; I need to do
Painful, yes, but that’s not all. If I also have a Foo::MumbleController that has the same pattern, I also have to specify
I understand that pattern matching is a problem when all that’s given is “variables”, but even if I could say something like
I wouldn’t need to repeat things as much. Unfortunately, there’s no way to set up the dynamic access to the recognized parts to construct the hash value.
I mean, unless I’m missing something. (uff da!)
17 Oct 2006
Dave, bring this up on the rails mailing list. Without seeing your full route definition file and the error you’re getting (and please, don’t post them here) it’s hard to know what the problem is. The :controller/:action/:target/:value route should definitely work as written.
17 Oct 2006
“That returning statement may look odd; it’s a method defined in ActiveSupport that makes it really easy to return a value, but only after performing some operations on it. You’ll find it used all over in Rails, so it’s worth getting familiar with it.”
Could you expand on this a little bit or at least point to where exactly it’s defined in ActiveSupport? I dug around (admittedly not very hard) a bit for it but didn’t see its definition.
Thanks, Jamis!
25 Oct 2006
Jim, your question inspired me to write an article about Object#returning. You can read about it in Mining ActiveSupport: Object#returning. Thanks!
27 Oct 2006