The maze book for programmers!
mazesforprogrammers.com

Algorithms, circle mazes, hex grids, masking, weaving, braiding, 3D and 4D grids, spheres, and more!

DRM-Free Ebook

The Buckblog

assorted ramblings by Jamis Buck

Under the hood: Rails' routing DSL

2 October 2006 — 6-minute read

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 of DynamicSegment. 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 a PathSegment.
  • 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

Jamis, It's great to see you blogging so frequently now. You are a great teacher and a series on the "mysterious" routing code is right up my alley. I enjoyed the first installment and look forward to the rest. Thanks, Zack
Jamis, Thanks for diving into this. While I can finally get around in the routes implementation, this is the kind of documentation we are missing. Understanding the design makes a great difference!
Hey I'm really glad you are posting this information. A lot of the rails internals can be a little confusing just because there is so much 'magic' going on. It also goes a long ways towards explaining the aesthetic of the underlying code, which is great.
Hi Jamis, Just wanted to add to the applause. Your writing style is clear and direct and I think Rails has really lacked a good, clear technical author to spelunk the internals. Thanks for the cap articles and here's to more on Rails! Alan
Thanks for the encouragement, all! I appreciate it. It's good to know I'm scratching an itch.
Best article on rails I've read in the last 6 months. Thanks Jamis.
From an eternal novice, many thanks for a well written and well explained article. And thanks to your pointing it out, I learned about "returning" and it was another opportunity to marvel at the beauty of ruby. I hope you will keep writing these wonderful explanations of the magic behind Rails. Much needed (imo), and much appreciated! -Amr
Thank you SO much for all of your recent posts! It's a real pleasure to read such awesome info from one of the masters... Please keep it up!
Thanks Jamis, Great article, this is the exact type of documentation that the community needs!
I agree, great stuff Jamis. Now we just need this article linked from the Rails documentation.
Thanks Jamis. Best article read on rails code to date!

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

map.connect ':controller/:action/:id'

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

map.connect ':controller/:action/:target/:value'

doesn’t work; I need to do

map.connect 'foo/bars/:action/:target/:value',
  :controller => 'foo/bars'

Painful, yes, but that’s not all. If I also have a Foo::MumbleController that has the same pattern, I also have to specify

map.connect 'foo/mumbles/:action/:target/:value',
  :controller => 'foo/mumbles'

I understand that pattern matching is a problem when all that’s given is “variables”, but even if I could say something like

map.connect 'foo/:sub_controller/:action/:target/:value',
  :controller => 'foo/:sub_controller'

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!)

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.

“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!

Jim, your question inspired me to write an article about Object#returning. You can read about it in Mining ActiveSupport: Object#returning. Thanks!