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: route generation in Rails

16 October 2006 — 20-minute read

I previously wrote about the implementation of the routing DSL in Rails, as well as the internals of route recognition. If you haven’t yet read (and understood) both of those articles, I strongly encourage you to do so before delving into this one, since it builds on the material presented in those.

Route generation is the last stop on this tour of the Rails routing code. It also happens to be the hairiest, most mysterious, and most difficult bit, so make sure you’ve got a recent version of the rails code handy to follow along with (this article outlines the implementation as of revision 5304). Also, don’t be afraid to take this article a bit at a time, and to go over it repeatedly. It’s dense stuff, and unless you’re already familiar with the routing code, it might be hard to swallow in one (or even two) sittings.

Before we dive headlong into this, let’s do a bit of warm-up, starting with some vocabulary.

Vocabulary

I’ll put these terms all up here, at the top of the article, to make it easier to refer back to them as you read. For the most part, these shouldn’t be difficult concepts to grasp, but by defining them here we can hopefully avoid ambiguity and confusion later.

  • default route. This is the traditional ”:controller/:action/:id” route that is defined by default in every new Rails application.
  • query parameters. This is the set of name/value pairs that were passed in via the query string, that part of the URL that follows the question mark. For example, in the URL ”/foo/bar/15?view=show”, the query string is “view=show”.
  • request parameters. This is the set of name/value pairs that were passed in via the body of a POST request.
  • path parameters. This is the set of name/value pairs that were extracted from the URL path itself for the current request. In other words, if the default route is used to recognize ”/foo/bar/15?view=show”, the path parameters will be { :controller => "foo", :action => "bar", :id => "15" }.
  • parameters. This is the combination of the query, request, and path parameter hashes. The controller’s params object references this hash.
  • parameter shell. This is a subset of a route definition’s key/value pairs. It includes all keys that are not explicitly referenced in the route’s mapping, and which have non-regex values. For example, the default route’s parameter shell would not include the :controller, :action and :id keys, because they are explicitly referenced in the route’s mapping.
  • path. For this article, “path” refers is the output of route generation, which is a string containing the path portion of a URL. In this article, “URL” will be loosely interchangeable with “path”.
  • route. In this article, this refers to the object that encapsulates the logic of generating a path, or URL. It is tempting to use “route” to refer to the output of route generation, but that way leads to madness and despair. Trust me.

Alright, with that out of the way, we can now take another step towards delving into route generation.

Route Generation

Route generation is the magic that happens when you call the link_to helper, or invoke a named route (foo_url or foo_path). In the former case, it takes some hash of options that describe the desired route, and it generates the URL that best matches the options. In the latter case, it attempts to generate the URL for the specific route you request. Ultimately, both approaches share 99% of the implementation.

It sounds like it should be simple: “take some options, and generate a path from them”. The devil, as ever, is in the details. Route generation attempts to be fairly smart about things, and in general it succeeds. Understanding the internals, though, will help you debug things when routing isn’t as smart as you’d like.

Some of the requirements that route generation must meet are:

  1. The generated path must consume the greatest possible number of keys from the hash you give. If you send in a hash with keys :a, :b and :c, ideally you’d like the routing code to select the route that uses all three of those keys. If that’s not possible, the generated path must consume as many of them as possible. Any left over keys get tacked on as query parameters, so another way of phrasing this is to say that the generated path must have the shortest possible list of query parameters.
  2. Namespaced controllers must be accounted for. Controller paths must be generated relative to the current controller’s module (if any). This is actually a legacy requirement, and results in frequent surprises when dealing with controllers in modules.
  3. Relevant parameters from the current request should be reused where possible. That is to say, if the current request is for ”/controller/action/15”, then generating a URL with only :id => 17 should give me ”/controller/action/17”, where the controller and action keys were implied from the current request.
  4. Route generation must be fast. The current implementation could certainly do better on this requirement. Every time you call link_to or invoke a named route, a URL gets generated, so a slow implementation can significantly impact the speed at which your page is rendered. Most of the complexity in the current implementation is due to the need to meet this requirement; it is surprisingly hard to get a fast route generator that also meets the above requirements!

Now, bear with me. As much as I’d love to plow onward through this stuff, we really need to pause one last time before we dig in. Item #3 above, though seemingly innocuous, is actually kind of a tricky point. To implement it, routing employs a concept known as “parameter expiration”, and before we go any further, we’d be wise to spend a minute or two explaining it.

Parameter Expiration

As you’ll see shortly, one of the first things the route generation process does is to build an expiry hash. This hash is used to determine which parameters can be reused from the current request. Each key in the expiry hash points to a boolean value, indicating whether or not that key is expired.

The expiration rules work something like this:

  • If a path parameter exists in the current request, but was not specified explicitly in the options given to generate, the parameter remains unexpired (and may be reused to generate the URL).
  • If a path parameter exists in the current request, and it has the same value in the options passed to generate, the parameter remains unexpired.
  • If a path parameter exists in the current request, but it’s value differs from that which was passed to generate, the parameter is expired and will not be reused to generate the URL. Furthermore, by expiring this value, any other key used later in the same route is also implicitly expired.

It sounds really funky in writing, but it’s honestly not that bad. Looking at a few examples may help to demonstrate how this expiry stuff works in practice. Consider the default route:


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

Now, given that route, let’s assume we receive a request for ”/foo/bar/15”. This will create the path parameter hash such that the :controller key is set to “foo”, the :action key to “bar”, and the :id key to “15”.

During the course of this request, we generate three URL’s.

1
2
3
<%= link_to "A", :id => 17 %>
<%= link_to "B", :action => "baz" %>
<%= link_to "C", :controller => "quux" %>

In the first case, the expiry hash will be set such that the :controller and :action keys are false, and the :id hash is true. This is because the options hash differs from the request’s recalled values only by the :id; the other two keys are absent altogether and thus are not expired. The generated URL, then, will have the same controller and action as the current request, differing only on the id: "/foo/bar/17".

For the second case, we now change the :action, leaving the others unchanged. This results in the expiry hash having the same keys as the previous example, but with :action set to true and the others set to false. When the URL is generated, this causes all keys following the action to be implicitly expired as well. In other words, because the :action key is expired, the :id also gets expired, so the generated route omits it, resulting in the path "/foo/baz". (This is why, if you are changing actions but want to use the same :id as the current request, you have to explicitly include it when you generate the URL.)

The third case changes the :controller and leaves the others. Using the same rule of implicit expiration as before, this expires all three keys, generating the path "/quux" (which is the same as "/quux/index", since “index” is the default action name).

If that’s not all perfectly clear right now, press on. Keep reading. It is not critical that you thoroughly understand parameter expiration at this point, and it may become clearer once you see how the route generation process uses the expiry hash.

With all that behind now, we’re finally ready to leap into the code itself. Here goes!

RouteSet#generate

Whether you use the link_to helper, the url_for method, or a named route, it all eventually gets funneled to RouteSet#generate. Because this article is long enough already, I leave those code paths for you to explore on your own.

RouteSet#generate begins on line 1190 of action_controller/routing.rb. It accepts three parameters:


def generate(options, recall={}, method=:generate)

The options parameter is a hash that will be used to generate the path. These are the keys and values you explicitly pass in, like :id => 17 in the parameter expiration example, above. The optional recall parameter, if given, is the hash of path parameters for the current request (which are being “recalled” to help generate the URL). Lastly, the method parameter specifies which method to call on each route. This is used primarily internally (see RouteSet#generate_extras). For now, we’ll just pretend it is always its default value, :generate.

Let’s take the body of the RouteSet#generate method one piece at a time:

1
2
3
4
5
named_route_name = options.delete(:use_route)
if named_route_name
  named_route = named_routes[named_route_name]
  options = named_route.parameter_shell.merge(options)
end

When you invoke a named route, the options hash it passes to #generate includes a special key (:use_route) that specifies the name of the route. First, that option is extracted, and if it exists, we look up the corresponding Route object and merge the route’s parameter shell with the options that were passed in.

Then:

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

The options_as_params method simply calls to_param on each value in options. This trick lets you do things like :id => user instead of :id => user.id, since ActiveRecord::Base#to_param just returns the id.

After that, we build the expiry hash, as described in the “Parameter Expiration” section, above. We use both the options hash (which is the set of explicit options passed to generate) as well as the recall hash (the path parameters from the current request) to build the expiry hash, and assign the result to expire_on.

Moving on:

1
2
3
4
5
6
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

This is legacy, here, and is only there because it’s how the original implementation of routes behaved. Oh, how we’d love to throw it out! Basically, all it does is ensure that if the controller has changed, it must change relative to any existing namespace for the recalled parameter. In other words, if you’re currently in the admin/get controller, and you ask for the set controller, you should get the admin/set controller. Unless, that is, the new controller is prefixed with a slash, e.g. /set. What a mess! I strongly suspect this behavior will go away with the advent of Rails 2.0.

Once we’ve danced that tango of relative controller paths, we can safely strip any leading slash from the controller name, and then merge the recall and options hashes:

1
2
options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/
merged = recall.merge(options)

This “merged” hash is what is used as the reference hash for route generation, until an expired parameter is encountered. At that point (as you’ll see) the original options hash becomes the reference, with the merged hash getting chucked out.

Moving right along, we now encounter the primary difference between routes generated by name, versus by hash:

1
2
3
4
if named_route
  path = named_route.generate(options, merged, expire_on)
  raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}" if path.nil?
  return path

If we’ve been asked to generate a named route, things are easy. We just pass the parameters to the route’s #generate method, and return the result. However, if we aren’t generating a specific named route, things are a bit trickier:

1
2
3
4
5
6
7
8
else
  merged[:action] ||= 'index'
  options[:action] ||= 'index'

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

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

First, we default the :action key to “index”. (You’ll notice this happens in multiple places in the code—a red flag that some refactoring is needed. Got a good idea for how to reduce the duplication? We’d love a patch!) After extracting both the controller and action values from the hash and making sure that both have been specified (either explicitly or implicitly, since we’re using the merged hash here), we find the set of routes that might possibly be able to generate the requested path:


routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]

Funky, funky stuff. The routes_by_controller method returns a multilevel hash of routes, keyed by controller, action, and the list of options passed in (sorted arbitrarily, but consistently). This is an optimization which causes only a subset of the routes to be considered by the process. (Otherwise, each request to generate the path would potentially have to try each defined route, which would be rather expensive.) Once the list of potential routes is identified, we iterate over them, trying to generate a path from each one until we succeed:

1
2
3
4
5
  routes.each do |route|
    results = route.send(method, options, merged, expire_on)
    return results if results
  end
end

If, when all is said and done, a route was not generated, we guiltily raise a RoutingError exception and blame the caller.

That’s the high-level view. From here on, things really start to get hairy.

Route#generate

For simplicity’s sake, let’s assume that the generation method used was :generate. That being the case, we now jump to line 411, to Route#generate.

1
2
3
4
def generate(options, hash, expire_on = {})
  write_generation
  generate options, hash, expire_on
end

Flashbacks to the recognition code! Route#generate calls write_generation to dynamically generate the new #generate method. As we did in the last article, let’s take a look at the code that gets generated for a few common scenarios. We’ll use the same three routes that we used before, to keep things simple:

1
2
3
4
5
6
7
8
9
10
ActionController::Routing::Routes.draw do |map|
  map.connect "/", :controller => "foo", :action => "index"

  map.connect "/foo/:action", :controller => "foo"

  map.connect "/foo/:view/:permalink", :controller => "foo",
    :action => "show", :view => /plain|fancy/,
    :permalink => /[-a-z0-9]+/,
    :conditions => { :method => :get }
end

For the first route, the following three methods get generated:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def generate(options, hash, expire_on = {})
  path, hash = generate_raw(options, hash, expire_on)
  append_query_string(path, hash, extra_keys(hash, expire_on))
end

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

def generate_raw(options, hash, expire_on = {})
  path = begin
    if hash[:controller] == "foo" && hash[:action] == "index"
      expired = false
      "/"
    end
  end
  [path, hash]
end

Three methods? The first two, as you can see, are defined in terms of generate_raw, which is where the real work is actually done. We’ll just focus on that one.

For now, kindly ignore the fact that this is a lot more code than this specific case actually needs; it’s simply an artifact of the code generation process. As you can see, all this method really does is test to see that the controller and action match the values given in the route definition, and then sets the path to ”/”. It then returns the path, as well as the “hash” (which is the “merged” hash). This hash is returned so that the caller (generate or generate_extras) can determine what query parameters need to be built.

That was pretty straight-forward. Let’s look at the next route, which is slightly more complex. (We’ll only show the generate_raw method, although all three methods are always generated for each route):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# map.connect "/foo/:action", :controller => "foo"
def generate_raw(options, hash, expire_on = {})
  path = begin
    if hash[:controller] == "foo"
      expired = false
      action_value = hash[:action] || "index"
      expired, hash = true, options if !expired && expire_on[:action]
      if action_value == "index"
        "/foo"
      else
        "/foo/#{CGI.escape(action_value.to_s)}"
      end
    end
  end
  [path, hash]
end

First, consider the route definition itself. What is it saying? It is saying that, if a URL is received in the form of /foo/bar, that the controller to use will be “foo” and the action to invoke will be “bar”. On the other hand, if a URL is received like /foo, then the action should default to “index”. (That just happens to be implicit in how routes behave: :action is generally optional, and defaults to “index” if it is not specified.)

So, how does that translate into code? First, we check the :controller key in the provided hash. (The hash parameter, again, is the “merged” hash, whereas options is the hash that was originally passed to the generate method.)

If the :controller key is “foo”, then we’ve satisified the explicit condition for the route. We then extract the :action value from the hash, defaulting to “index” if it isn’t given.

The next line references the expiry hash, expire_on. It only has a minor effect on this route, basically setting the hash variable to the options variable if the :action key has expired. (Remember, a key is expired if it’s new value differs from what was passed into the request.) In effect, all recalled values are discarded if the action has expired. (You’ll see where this comes in when it comes time to generate any query parameters. It also makes a difference in more complicated routes, as you’ll see.)

If the action is “index”, then we omit it and simply generate ”/foo” as the path. Otherwise, we include the action in the path. Either way, we return an array of the new path, and the hash variable (which will be either the “merged” hash, or the options hash, if any parameter expiration occurred).

Whew! Make sure you understand that one before proceeding. (You can safely proceed even if the expiry stuff doesn’t make any sense. Hopefully you’ll understand more of what it is supposed to do before the end of the article.)

Now, for the final route:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# map.connect "/foo/:view/:permalink", :controller => "foo",
#   :action => "show", :view => /plain|fancy/,
#   :permalink => /[-a-z0-9]+/,
#   :conditions => { :method => :get }
def generate_raw(options, hash, expire_on = {})
  path = begin
    if hash[:controller] == "foo" && hash[:action] == "show"
      expired = false

      view_value = hash[:view] 
      return [nil,nil] unless view_value && /\Aplain|fancy\Z/ =~ view_value
      expired, hash = true, options if !expired && expire_on[:view]

      permalink_value = hash[:permalink] 
      return [nil,nil] unless permalink_value && /\A[-a-z0-9]+\Z/ =~ permalink_value
      expired, hash = true, options if !expired && expire_on[:permalink]

      "/foo/#{CGI.escape(view_value.to_s)}/#{CGI.escape(permalink_value.to_s)}"
    end
  end
  [path, hash]
end

We begin by testing the parameter shell. If those do not match the values given in the “merged” hash, then this route does not generate the desired URL, and we just return nil for the path. Otherwise, we jump in and begin testing the constrained values.

First, we extract the value of the :view parameter. If that value is nonexistent (or nil), or if it does not match the regular expression that was given, then we just return nil to indicate that the path could not be generated by this route.

Next, we check whether or not the hash should be expired based on this key. If we haven’t yet expired the hash (which will be true at this point) and the :view key has been expired, then we expire the merged hash by replacing it with the options hash, and we set expired to true. (Note that by replacing the merged hash with the original options hash, we have effectively expired all subsequent keys in the route—in other words, when we check the next value in the hash, :permalink, it will be pulled from the options hash and not from the value used in the last request, if the :view value has changed.)

Ok, the final stretch. We now grab the :permalink value from the hash, and do the same hokey-pokey with it. If it is present, and it matches the associated regular expression, we again check for whether or not to expire the hash.

If everything checks out, we build the path for this route using the extracted values, and return!

So, that’s what the generated code looks like. The task now is to decipher how that code gets generated.

Route#write_generation

We begin with the Route@write_generation method. The relevant part is the first few lines:

1
2
body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements

Here, body is used to accumulate the text that will be evaluated to build the generate_raw method. The first line builds the collection of statements that actually generates the route’s path. The second line then wraps the first, if there are any prerequisites that must be met first.

The generation_extraction method, which builds the sequence of statements that “extract” the path from the options, is blessedly short:

1
2
3
4
5
def generation_extraction
  segments.collect do |segment|
    segment.extraction_code
  end.compact * "\n"
end

It just takes all of the segments for the route, asks them for their extraction code, removes the nils, and then joins the results with newlines. Elegant! (The “extraction code”, by the by, is the code that you saw in the third example above, where the value is pulled from the hash, assigned to a variable, validated, and then tested for parameter expiration.)

The default Segment#extraction_code method returns nil, which means that, by default, routing segments do not encapsulate any data that needs extracting. The only exception to this rule is the DynamicSegment class, which overrides the default extraction_code method with the following:

1
2
3
4
5
6
def extraction_code
  s = extract_value
  vc = value_check
  s << "\nreturn [nil,nil] unless #{vc}" if vc
  s << "\n#{expiry_statement}"
end

The call to the extract_value method creates the default assignment line, which pulls the value from the hash and assigns it to a variable. The value_check method builds the condition that enforces the constraints for this segment, like comparing the extracted value against a regular expression. The two are then combined, and the expiry_statement is appended, which spits out the default one-line expiry test (as shown above). The result is then returned and collected with the other segments.

The requirement generation proceeds similarly, but uses the route’s requirements, rather than its segments.

1
2
3
4
5
6
7
8
9
10
11
def generation_requirements
  requirement_conditions = requirements.collect do |key, req|
    if req.is_a? Regexp
      value_regexp = Regexp.new "\\A#{req.source}\\Z"
      "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
    else
      "hash[:#{key}] == #{req.inspect}"
    end
  end
  requirement_conditions * ' && ' unless requirement_conditions.empty?
end

Each of the route’s requirements (which are, essentially, any values in the options that were not referenced in the route’s segments) are tested, and a condition built from them. If the value for a requirement is a regular expression, a regular expression test will be generated, otherwise a simple equality is used. The resulting strings are joined with ' && ', and returned.

Lastly, generation_structure is invoked to build the code that actually combines the different segments to construct the resulting path:

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

For a single line of code, it hides a healthy amount of complexity. But we’re on the final stretch! Hang on just a bit longer as we walk through this last chunk of code.

Route#generation_structure

We can make this clearer if you use a real segment list as an example. Consider the third route in the example given earlier. The segment list would look something like this:

  1. DividerSegment /
  2. StaticSegment foo
  3. DividerSegment /
  4. DynamicSegment :view
  5. DividerSegment /
  6. DynamicSegment :permalink
  7. DividerSegment /

Using this list as a reference, we can tackle generation_structure. It grabs the last segment in the list, and then invokes the string_structure method of it, passing in the list of all previous segments. (This “string structure” it refers to is the code that constructs the actual path from the component segments.) Let’s look at the default Segment#string_structure method:

1
2
3
4
5
def string_structure(prior_segments)
  optional? ?
    continue_string_structure(prior_segments) :
    interpolation_statement(prior_segments)
end

In this case, the last DividerSegment (#7) is the target of the method, and divider segments are always optional. Thus, we take the first branch, and continue_string_structure get called.

1
2
3
4
5
6
7
8
 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

The prior_segments list will contain, in this case, segments 1 through 6, so the list is definitely not empty. We slice the list to get the new_priors list (which will contain segments 1 through 5), and then invoke string_structure on segment #6, the :permalink segment.

Now, DynamicSegment has it’s own version of string_structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

Well, our :permalink segment is not optional, since it has no default value. Thus, we take the second branch and build the interpolation_statement beginning with this segment.

Let’s take a peek at the default Segment#interpolation_statement method. It accumulates all prior segments and collects the results of their interpolation_chunk methods:

1
2
3
4
5
def interpolation_statement(prior_segments)
  chunks = prior_segments.collect { |s| s.interpolation_chunk }
  chunks << interpolation_chunk
  "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}"
end

Watch out for that last mouthful, the call to all_optionals_available_condition. It “simply” examines the list of segments given (segments 1 through 5, in this case) and determines which of them are optional. It then builds an if statement using the values from those segments, and tacks it onto the joined chunks array. This prevents the path from being built if any of the optional segments were omitted (but only considering the optional segments that precede the current segment).

Confusing?

Yes!

Keep in mind that the interpolation is built starting with the last segment. If that segment is optional, the the segment preceding it is used as the starting point, and so forth, until a non-optional trailing segment is found. Thus, this all_optionals_available_condition method makes sure that all the so-called “optional” segments that precede that (non-optional) tail segment have values. In other words, even if a segment thinks it is optional, it cannot be missing if there are any mandatory segments that follow it. Follow that? Wild stuff, man!

Wrapping Up

Whew! If you’ve made it this far, stop and congratulate yourself. That was dense stuff!

You should now have a better-than-general idea of how the Route#generate_raw method is built. That method gets called by the new Route#generate method:

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

It takes the path and hash that were generated, and simply passes them on to the append_query_string method. In the process, it calls the extra_keys method, which determines which of the keys in the hash were not used to generate the path. Those left-over keys, then, get tacked on as query parameters by the append_query_string method.

To illustrate: assume we are generating a path using the default route, and we pass in a few additional options:

1
2
link_to "A", :controller => "foo", :action => "bar",
  :id => "15", :view => "show", :format => "xml"

The generated path would be:


/foo/bar/15?view=show&format=xml

In other words, controller, action, and id all wind up in the path itself (since the default route references those keys and consumes them when generating the URL). The remaining keys, :view and :format, were not consumed, and thus wind up as query parameters.

Finish!

And that’s that. As before, there is a lot that I haven’t covered, but you should at least have some idea of where to look for answers at this point. If, in your own hacking of the routing code, you come upon a way to make route generation faster, please do share! Also, the current implementation of route generation is not very extensible. It’s difficult to monkeypatch, and that means it is hard for people to write plugins that have custom route generation functionality. If you’ve got a solution to that conundrum, I’d love to hear from you.

Reader Comments

I think I’ve found a bug in RouteSet#generate.

The following code is supposed to skip routes that don’t match:

routes.each do |route| results = route.send(method, options, merged, expire_on) return results if results end

This works with method=:generate since Route#generate either returns a string or nil.

However, Route#generate_extras and Route#generate_raw always return an array. The elements of this array will be nil if the Route doesn’t match.

I’ve documented a problem this causes and provided a patch in http://dev.rubyonrails.org/ticket/6300.

Good catch, Phil!

Jamis, thanks for this series—it’s awesome. I always wondered how this worked. Seeing how the language enables it just gives me that much more respect for Ruby.