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.
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.
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="/">]>
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.
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 {}
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.
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.
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.
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.
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!
18.117.230.81