Chapter 5. Reflecting on Rails Routing

 

You are in a maze of twisty little passages, all alike.

 
 --Adventure (late 70s-era computer game)

In this context, reflecting doesn’t mean pondering; it means examining, testing, and troubleshooting. The routing system gives you numerous points of entry for reflection of this kind—and there are some handy plugins that can help you further.

We’re not going to look at every technique and/or plugin, but in this chapter you’ll become acquainted with several important facilities that will help you make sure your routes are doing what you think they’re doing, and how to figure out why they’re not if they’re not.

We’ll also take a look at the source code for the routing system, including the RESTful routing facilities.

Examining Routes in the Application Console

Coming full circle to the approach we took at the beginning of the book with respect to the dispatcher, you can examine the workings of the routing system in the application console. This can help with troubleshooting, and can also help you learn about the routing system.

Dumping Routes

Let’s start by examining all the available routes. To do this, you need to get hold of the current RouteSet object:

$ ruby script/console
Loading development environment.
>> rs = ActionController::Routing::Routes

You’ll see a rather huge amount of output—a screen dump of all the defined routes. You can, however, get this dump in a more readable form:

>> puts rs.routes

This will give you a kind of chart of all the defined routes:

GET    /bids/              {:controller=>"bids", :action=>"index"}
GET    /bids.:format/      {:controller=>"bids", :action=>"index"}
POST   /bids/              {:controller=>"bids", :action=>"create"}
POST   /bids.:format/      {:controller=>"bids", :action=>"create"}
GET    /bids/new/          {:controller=>"bids", :action=>"new"}
GET    /bids/new.:format/  {:controller=>"bids", :action=>"new"}
GET    /bids/:id;edit/     {:controller=>"bids", :action=>"edit"}
GET    /bids/:id.:format;edit  {:controller=>"bids", :action=>"edit"}

# etc.

The amount of information may be greater than you need, but it can be enlightening to see it in this form. You get a graphic sense of the fact that each route includes a request method, a URL pattern, and parameters specifying the controller/action sequence.

You can also look at named routes in a similar format. With named routes it pays to neaten up the format a bit. As you iterate through the routes, you get a name and a route for each one. You can use this information to output a chart:

rs.named_routes.each {|name, r| printf("%-30s  %s
", name, r) }; nil

The nil at the end is to stop irb from outputting the actual return value of the call to each, which would push the interesting stuff off the screen.

The result looks like this (modified to look decent on this page):

history  ANY    /history/:id/  {:controller=>"auctions",
                                :action=>"history"}
new_us   GET    /users/new/    {:controller=>"users", :action=>"new"}
new_auction GET /auctions/new/ {:controller=>"auctions",
:action=>"new"}

# etc.

The upshot of this is that you can get a lot of routing information in the console, and slice and dice it any way you need. But what about the “raw” information? That original screen dump contained some important elements too:

#<ActionController::Routing::Route:0x275bb7c
@requirements={:controller=>"bids", :action=>"create"},

@to_s="POST   /bids.:format/ {:controller=>"bids",
:action=>"create"}",
@significant_keys=[:format, :controller, :action],
@conditions={:method=>:post},
@segments=[#<ActionController::Routing::DividerSegment:0x275d65c
@raw=true, @is_optional=false, @value="/">,
#<ActionController::Routing::StaticSegment:0x275d274
@is_optional=false, @value="bids">,
#<ActionController::Routing::DividerSegment:0x275ce78 @raw=true,
@is_optional=false, @value=".">,
#<ActionController::Routing::DynamicSegment:0x275cdc4
@is_optional=false, @key=:format>,
#<ActionController::Routing::DividerSegment:0x275c798 @raw=true,
@is_optional=true, @value="/">]>

Anatomy of a Route Object

The best way to see what’s going on here is to look at a YAML (Yet Another Markup Language) representation of this particular route. Here’s the output of a to_yaml operation, together with some comments. There’s a lot of information here, but you can learn a lot by looking it over. You’re seeing a kind of X-ray of how a route is constructed.

# The whole thing is a Route object

-- !ruby/object:actionController::Routing::Route

# This route only recognizes PUT requests.
conditions:
  :method: :put

# The basic chain of events upon recognition, and the hooks for
# matching when generating.
requirements:
  :controller: bids
  :action: update

# The segments. This is the formal definition of the pattern string.
# Everything is accounted for, including the dividers (the forward
# slashes).

# Note that each segment is an instance of a particular class:
# DividerSegment, StaticSegment, or DynamicSegment.

# If you read along, you can reconstruct the possible values
# of the pattern.

# Note the regexp field, automatically inserted, constraining the
# possible values of the :id segment.
segments:
- !ruby/object:actionController::Routing::DividerSegment
  is_optional: false
  raw: true
  value: /
- !ruby/object:actionController::Routing::StaticSegment
  is_optional: false
  value: auctions
- !ruby/object:actionController::Routing::DividerSegment
  is_optional: false
  raw: true
  value: /
- !ruby/object:actionController::Routing::DynamicSegment
  is_optional: false
  key: :auction_id
- !ruby/object:actionController::Routing::DividerSegment
  is_optional: false
  raw: true
  value: /
- !ruby/object:actionController::Routing::StaticSegment
  is_optional: false
  value: bids
- !ruby/object:actionController::Routing::DividerSegment
  is_optional: false
  raw: true
  value: /
- !ruby/object:actionController::Routing::DynamicSegment
  is_optional: false
  key: :id
  regexp: !ruby/regexp /[^/;.,?]+/
- !ruby/object:actionController::Routing::DividerSegment
  is_optional: true
  raw: true
  value: /
significant_keys:
- :auction_id
- :id
- :controller
- :action

# (This should be on one line; it's split here for formatting
reasons.)
to_s: PUT /auctions/:auction_id/bids/:id/
         {:controller=>"bids" :action=>"update"}

The storage of segments in a collection enables the routing system to perform recognition and generation, since the segments can be traversed both in an attempt to match the segments from a candidate URL, and to output a URL using the segments as a template or blueprint.

You can, of course, find out more about the inner workings of routes by looking in the routing source code. We’re not going to go into it in depth here, but have a look at the files routing.rb and resources.rb in the ActionController source tree. You’ll see the definitions of Routing, RouteSet, the various Segment classes, and more. If you want to learn more about how the source code functions, there’s a series of blog posts by Jamis Buck, Rails core team member, which will give you a good guided tour.[1]

There’s more you can do in the console: You can directly execute recognition and generation of URLs.

Recognition and Generation in the Console

To do manual recognition and generation in the console, let’s first scope our console session to the RouteSet object. (If you’ve never seen this technique, you’ll be learning a nice IRB trick along the way.)

$ ./script/console
Loading development environment.
>> irb ActionController::Routing::Routes
>>

By giving the irb command inside IRB, we’ve set the default object—self—to the route set. This saves some typing as we proceed to give commands.

To see a route generated from parameters, feed the parameters to the generate method. Here are some annotated examples.

Here’s a nested resource routes for bids. The create action generates a collection URL; there’s no :id field. But there is an :auction_id field, to achieve the nesting.

>> generate(:controller => "bids", :auction_id => 3, :action =>
 "create")
=> "/auctions/3/bids"

Here are our two custom action routes for bids, nested inside users. In each case (retract and manage), providing the appropriate action name is enough to trigger the use of the extra path segment in the URL.

>> generate(:controller => "bids", :user_id => 3, :id => 4, :action =>
 "retract")
=> "/users/3/bids/4/retract"
>> generate(:controller => "bids", :user_id => 3, :action => "manage")
=> "/users/3/bids/manage"

Remember the bid history action “history” in the auctions controller? Here’s how to generate the URL for it.

>> generate(:controller => "auctions", :action => "history", :id => 3)
=> "/history/3"

These next two examples use the item_year route that requires a year parameter consisting of four digits. Note that the generation fails when the year doesn’t match this pattern; the year value is added to the query string, rather than included as a URL segment.

>> generate(:controller => "items", :action => "item_year", :year =>
1939)
=> "/item_year/1939"
>> generate(:controller => "items", :action => "item_year", :year =>
19393)
=> "/items/item_year?year=19393"

You can go in the other direction too, starting with paths and seeing how they play out in terms of controller, action, and parameters, according to the route recognition system.

The top-level path, as defined by us in routes.rb:

>> recognize_path("/")
=> {:controller=>"auctions", :action=>"index"}

Similar results come from the auctions resource, called in the plural with the GET request method.

>> recognize_path("/auctions", :method => :get)
=> {:controller=>"auctions", :action=>"index"}

With the POST method, the result is different: It’s routed to the create action.

>> recognize_path("/auctions", :method => :post)
=> {:controller=>"auctions", :action=>"create"}

The same logic applies to a plural POST request in a nested route.

>> recognize_path("/auctions/3/bids", :method => :post)
=> {:controller=>"bids", :action=>"create", :auction_id=>"3"}

The custom actions are recognized and broken down into the appropriate controller and action parameters.

>> recognize_path("/users/3/bids/1/retract", :method => :get)
=> {:controller=>"bids", :user_id=>"3", :action=>"retract", :id=>"1"}

Here’s our history route, bound to the auctions controller.

>> recognize_path("/history/3")
=> {:controller=>"auctions", :action=>"history", :id=>"3"}

The item_year route only recognizes paths with four-digit numbers in the :year position. In the second of these two examples, the system reports failure: there’s no appropriate route.

>> recognize_path("/item_year/1939")
=> {:controller=>"items", :action=>"item_year", :year=>"1939"}
>> recognize_path("/item_year/19393")
ActionController::RoutingError: no route found to match
"/item_year/19393" with {}

Named Routes in the Console

You can also execute named routes in the console. The easiest way to do this is to include the module ActionController::UrlWriter, and then set the default host value to anything (just to suppress errors):

>> include ActionController::UrlWriter
=> Object
>> default_url_options[:host] = "example.com"
=> "example.com"

Now you can call named routes and see their return values—that is, the URLs they generate.

>> auction_url(1)
=> "http://example.com/auctions/1"
>> formatted_auction_url(1,"xml")
=> "http://example.com/auctions/1.xml"
>> formatted_auctions_url("xml")
=> "http://example.com/auctions.xml"

As always, the application console can help you both learn and debug. If you keep your routes.rb file open in one window and the console in another, you’ll make rapid progress in getting a feel for how the recognition and generation of routes dovetail with each other. Sadly, the console doesn’t seem to reload the routing table automatically. Not even the reload! method seems to force it to be read again.

The console is good for learning and ad-hoc testing, but there are also facilities for the more systematic process of testing routes.

Testing Routes

The testing system gives you some facilities for testing routes:

  • assert_generates

  • assert_recognizes

  • assert_routing

The third of these, assert_routing, is really the first two rolled into one: You give it a path and some parameters, and it tests both the recognition of the path, making sure it resolves to the parameters you’ve given, and the generation of the path from the parameters. Testing and specifying of routing is covered in detail in Chapters 17, “Testing,” and 18, “RSpec on Rails,” of this book.

Exactly which tests you write for your routes, and how many tests, is up to you. Ideally, of course, your test suite would include at least every permutation that you’re actually using in your application. If you want to see a rather thorough set of routing tests, have a look at the file routing.rb, in the test subdirectory of your ActionPack installation. At last count it was 1881 lines long. It’s testing the actual framework, so you’re not expected (or advised!) to duplicate it in your own tests—but, like the other Rails framework test files, it might give you some ideas and will certainly illustrate the process of test-driven development for you.

A Word about Argument Syntax

Don’t forget Ruby’s rule about hashes in argument lists:

If the last argument in the list is a hash, you can omit the curly braces.

That’s why you can do this:

assert_generates(user_retract_bid_path(3,1),
                 :controller => "bids",
                 :action => "retract",
               :id => "1", :user_id => "3")

If you use a hash anywhere except in the last position, you have to wrap it in curly braces. That’s why you must do this:

assert_recognizes({:controller => "auctions",
                   :action => "show",
                   :id => auction.id.to_s },
                   auction_path(auction))

Here, auction_path(auction) is the last argument, so the hash has to use the curly braces.

If you ever get slightly mysterious syntax errors pertaining to your argument lists, make sure you’re not violating the hash rule.

The Routing Navigator Plugin

Rick Olson, one of the Rails core developers, has written a plugin called Routing Navigator that gives you a deluxe version of the kind of information you can get by examining routes in the console—right in your browser.

To install the Routing Navigator plugin, give this command from the top level of your Rails application directory:

./script/plugin install
   http://svn.techno-weenie.net/projects/plugins/routing_navigator/

Now, you have to tell one or more controllers that you want them to show you the route information you crave. In auction_controller.rb, for example, put this line:

routing_navigator :on

at the beginning of the controller class definition (right where you’d put before_filters and other class methods). This is definitely something that you want to do on an occasional basis during development; you don’t want to leave routing navigation on in a production application.

The Recognize and Generate buttons give you input boxes where you can put paths (for recognition) or parameters (for generation) and have the operations performed for you. It’s similar to doing the same thing in the application console, but a lot slicker.

The last button is Routing Navigator. This takes you to a new page, which includes every single route, named or not, defined for your application. Above the list of routes is the Routing Viewer utility. You can input paths and/or route parameters, and perform recognition and generation operations.

The list of all available routes at the bottom of the viewer can get quite long. But you can filter it, using another of the input windows—the one labeled “YAML to filter routes by requirements.” For example, if you put controller: bids into this window, and hit the Filter button, the list at the bottom will be refreshed to include only those routes that pertain to the bids controller.

The Routing Navigator is a fine debugging tool, and it’s worthwhile to spend some time working with it just for learning.

Conclusion

This brings us to the end of our tour of Rails routing, RESTful and otherwise. As you develop Rails applications, you’re sure to find some favorites among the various idioms and techniques available to you; and there’s a whole world of existing code to which you can look for examples. If you keep the fundamentals in mind, you’ll find that your routing skills will develop nicely and will greatly enhance the look and logic of your code.

Happy routing!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.46.69