Chapter 15. Routing

Rails routing can shock developers who are used to putting their code in files wherever they want to put them. After the directory-based approach of traditional HTML and template-based development, Rails’ highly structured approach looks very strange. Almost nothing, except for a few pieces in the public folder, is anywhere near where its URI might have suggested it was. Of course, this may not be so shocking if you’ve spent a lot of time with other frameworks or blogs—there are many applications that control the meanings of URIs through mechanisms other than the file system.

Note

If you prefer to read “URI” as the older and more familiar URL, that’s fine. Everything works the same here. (And the core method Rails uses to generate URIs is, of course, url_for, in the UrlModule.)

Rails routing turns requests to particular URIs into calls to particular controllers and lets you create URIs from within your applications. Its default routing behavior, especially when combined with resource routes generated through scaffolding, is often enough to get you started building an application, but there’s a lot more potential if you’re willing to explore Rails routing more directly. You can create interfaces with memorable (and easily bookmarkable) addresses, arrange related application functionality into clearly identified groups, and much, much more.

What’s more, you can even change routes without breaking your application’s user interface, as the routing functionality also generates the addresses that the Rails view helper methods put into your pages.

Warning

Changing routing can have a dramatic impact on the web services aspect of your applications. Programs that use your applications for XML-based services aren’t likely to check the human interface to get the new address, and won’t know where to go if you change routing. Routing is effectively where you describe the API for your projects, and you shouldn’t change that too frequently without reason.

Creating Routes to Interpret URIs

Rails routing is managed through a single file, config/routes.rb. When Rails starts up, it loads this file, using it to process all incoming requests.

Note

If you’re in development mode, which you usually are until deployment, Rails will reload routes.rb whenever you change it. In production mode, you have to stop and restart the server.

The default routes.rb contains a lot of help information that can get you started with the routes for your application, but it helps to know the general scheme first. In routing, Rails takes its fondness for connecting objects through naming conventions and lets you specify the conventions. Doing that means learning another set of conventions, of course!

Specifying Routes with map.connect

The easiest place to start is with the default rules, as you’ve probably already written code using them without having examined them too closely. They are near the bottom of the file and get called if nothing above them matched. You’ll always want to put higher-priority routes above lower-priority ones, since the first match wins. The default rules look like:

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

The map.connect method is the foundation of routing, though what it does can be mysterious until you compare it to some actual URIs. (Again, for the purposes of this chapter, if you’d rather read URI as URL, that’s fine.) For example, if your Rails server on localhost, port 3000, had a controller named people that had an action named show, and you wanted to apply that to the record with the id of 20, this rule would let you do that with a call to:

http://localhost:3000/people/show/20

When Rails gets this, it looks for a rule that looks like it matches the URI structure. It checks the default rules last, but when it encounters the first default rule, Rails knows from this to set :controller to people, :action to show, and :id to 20. The symbols (prefaced with colons) act as matching wildcards for the routing. Rails uses that information to call the PeopleController’s show method, passing it 20 as the :id parameter.

If a request comes in that would match ':controller' or ':controller/', Rails assumes that the next piece would be index, much as web servers expect index.html to be a default file. The :action will be set to index. Also, Rails ignores the name of the web server in routing, focusing on the parts of the URI after the web server name.

Routing rules also work in reverse. The link_to helper method and the many other methods link_to supports can take a :controller, an :action, and optionally even an :id, and generate a link to a URL for accessing them. For example:

link_to :controller => 'people', :action => 'show', :id => 20

would, working with the default rules, produce:

http://localhost:3000/people/show/20

The second rule is much like the first, with one piece of extra functionality. If a user wanted to request XML specifically, she could write:

http://localhost:3000/people/show/20.xml

Because these match the second map.connect rule, Rails will set :controller to people, :action to show, :id to 20, and :format to xml. Then it uses that information to call the PeopleController’s show method, passing it 20 as the :id parameter and xml as the :format parameter.

If your controller checks the :format parameter—Chapter 5 examined respond_to, the easiest way to do this—and the value is one you’ve checked for, your controller can send a response in the requested format. This isn’t limited to HTML or XML—you can specify other formats through the extension. If your controller supports them, visitors will get what they expect. If not, they might be disappointed, but nothing should break.

Note

You could and probably should also specify the format through the MIME content-type header in the HTTP request, but that doesn’t get checked in ordinary Rails routing.

There are many different ways to use map.connect. The approach that the default rules take—presenting a string filled with symbols that connect to pieces of a URI—is simple, but a rather blunt instrument. The map.connect method offers another approach that lets you specify URIs quite precisely: explicit specification of the URI and directions for where to send its processing. This looks like:

map.connect 'this/uri/exactly', :controller => "myController", :action =>
"myAction"

Using this rule, if a request comes in to a Rails server at localhost:3000, looking like:

http://localhost:3000/this/uri/exactly

Rails will call the myController controller’s myAction method to handle the request.

Note

In many older Rails applications and in documentation, you’ll often see map.connect used with a blank string as its first argument, as in:

map.connect '', :controller => "somewhere", :action
=> "something"

This tells Rails what to do if the URI points at the top level of the server. It still works, but using map.root, covered later in this chapter, is now considered better practice. (You also need to delete index.html from the public directory.)

While explicitly declaring mappings from individual URIs to particular controller actions is certainly precise, it’s also not very flexible. Fortunately, you can mix symbols into the strings however you think appropriate to create combinations that meet your needs. For example, you might have a route that looks like:

map.connect ':action/awards', :controller => 'prizes'

if Rails encountered a URI like:

http://localhost:3000/show/awards

Then it would route the call to the show method of the prizes controller.

The map.connect method supports one other important technique. Calls to it aren’t limited to the :controller, :action, :id, and :format parameters. You can call it with any parameters you want—:part_number, :ingredient, or :century, for example—and those parameters will be sent along to the controller as well. What’s more, you can mix those symbols into the URI string for automatic extraction, making it possible to create routes like:

map.connect 'awards/:first_name/:last_name/:year', :controller => 'prizes', action
=> 'show'

Then it would route the call to the show method of the prizes controller, with the arguments :first_name, :last_name, and :year.

A Domain Default with map.root

Often when prototyping, developers (and especially designers) like to start with the top page in a site, the landing page visitors will see if they just enter the domain name. The vision for this “front door” often sets expectations for other pages in the site, and the front door gets plenty of emphasis because it’s often the first (or only) page users see. Even in an age where Google sends users to pages deep inside of a site, users often click to “the top” to figure out where they landed.

There are two ways to build this front door in Rails. The first way, which may do well enough at the outset, is to create a static HTML file that is stored as public/index.html. That page can then have links that move users deeper into your application’s functionality. It’s more likely, however, that projects will quickly outgrow that, as updating a static page in an otherwise dynamic application means extra hassle when things change.

The second approach deletes public/index.html and uses routing to specify where to send users who visit just the domain name. Before Rails 2.0, this was done by specifying map.connect for an empty string, but Rails 2.0 introduced map.root, a cleaner way of making the connection. The map.root method looks just like map.connect, but doesn’t have the first method. If you want visitors to the domain name to receive a page from the entry method of the welcome controller, you could write:

map.root :controller => welcome, :action => entry

You could also specify :id and any other parameters you want, just as with map.connect.

Route Order and Priority

Using wildcards makes it likely—even probable—that more than one routing rule applies to an incoming URI. This could have produced an impenetrable tangle, but fortunately Rails’ creators took a simple approach to tie-breaking: rules that come earlier in the routes.rb file have higher priority than rules that appear later. Rails will test a URI until it comes to a match, and then it doesn’t look any further.

In practice, this means that you’ll want to put more specific rules nearer the top of your routes.rb file and rules that use more wildcards further down. That way the more specific rules will always get processed before the wildcards get a chance to apply themselves to the same URI.

Named Routes

While you could use map.connect for all of your routes, you’d miss out on a lot of convenience facilities Rails could provide your application. By naming routes, you gain helper methods for paths and URLs, making your application more robust and more readable.

How do you name a route? It’s simple—just replace connect with the name of your route. For example, to create a route named login, you could write:

map.login '/sessions/new', :controller => 'sessions', :action => 'new'

Once you’ve done this, you’ll have two new helper methods, login_path and login_url. The first will return /sessions/new and the second http://localhost:3000/sessions/new (if you’re running it in the default server). That may not seem that important, but once you have something like this scattered through your views:

<%= link_to "Login", login_path %>

it’s nice to be able to change where those point just by modifying a single line of the routes.rb file.

Note

If you have a lot of named routes pointing to the same controller, you might want to look into map.with_options, which sets up a block providing scope to a collection of named routes.

Globbing

While it’s useful to have the default route retrieve an id value and pass it to the controller, some applications need to pull more than one component from a given URI. For example, in an application that makes use of taxonomies (trees of formal terms), you might want to support those tree structures in the URI. If, for example, “floor” could refer to “factory floor” in one context, “dance floor” in another context, and “price floor” in yet another context, you might want to have URIs that looks like:

http://localhost:3000/taxonomy/factory/floor
http://localhost:3000/taxonomy/dance/floor
http://localhost:3000/taxonomy/price/floor

The only piece that the routing tool needs to be able to identify is taxonomy, but the method that gets called also needs the end of the URI as a parameter. A route that can process that might look like:

map.taxonomy 'taxonomy/*steps' :controller => 'taxonomy', action => 'showTree'

The asterisk before steps indicates that the rest of the URI is to be “globbed” and passed to the showTree method as an array, accessible through the :specs parameter. The showTree method might then start out looking like:

def showTree
  steps = params [:steps]
....
end

If the method had been called via http://localhost:3000/taxonomy/factory/floor, the steps variable would now contain [ 'factory', 'floor' ]; if called via http://localhost:3000/taxonomy/factory/equipment/mixer, the steps variable would now contain [ 'factory', 'equipment', 'mixer' ]. Globbing makes it possible to gather a lot of information from a URI.

Warning

Globs can only appear at the end of a match string. You can’t glob something and then return to processing something in the URI after the glob has done its work, since everything that appears from the glob marker onward will be put in the array.

Regular Expressions and Routing

While Rails is inspecting incoming request addresses, you might want to have it be a little more specific. For example, you might create a route that checks to make sure that the id values are numeric, not random text, and presents an error page if the id value has problems. To do this, you can specify regular expressions in parameters for your routes:

map.connect ':controller/:action/:id', :id => /d+/
map.connect ':controller/:action/:id', :controller => 'errors', :action => 'bad_id'

The first rule looks like the default rules, but checks to make sure that the :id value is composed of digits. (Regular expressions are explained in Appendix C.) If the id is composed of digits, the routing goes on as usual to the appropriate :controller and :action with the :id as a parameter. If it isn’t, Rails proceeds to the next message, which sends the user to a completely different errors controller’s bad_id method.

Mapping Resources

If you’re building REST-based applications, you will become very familiar with map.resources. It both saves you tremendous effort and encourages you to follow a common and useful pattern across your applications. Chapters 5 and 9 have already explored how REST works in context, but there are a few more options you should know about and details to explore. A simple map.resources call might look like:

map.resources :people

That one line converts into 14 different mappings from calls to actions. Each REST-based controller has seven different methods for handling requests, and the routing has to handle cases with and without a :format property. Table 15-1 catalogs the many things this call creates.

Table 15-1. Routing created by a single map.resources call

Name

HTTP method

Match string

Parameters

people

GET

/people

{:action=>"index", :controller=>"people"}

formatted_people

GET

/people.:format

{:action=>"index", :controller=>"people"}

POST

/people

{:action=>"create", :controller=>"people"}

POST

/people.:format

{:action=>"create", :controller=>"people"}

new_person

GET

/people/new

{:action=>"new", :controller=>"people"}

formatted_new_person

GET

/people/new.:format

{:action=>"new", :controller=>"people"}

edit_person

GET

/people/:id/edit

{:action=>"edit", :controller=>"people"}

formatted_edit_person

GET

/people/:id/edit.:format

{:action=>"edit", :controller=>"people"}

person

GET

/people/:id

{:action=>"show", :controller=>"people"}

formatted_person

GET

/people/:id.:format

{:action=>"show", :controller=>"people"}

PUT

/people/:id

{:action=>"update", :controller=>"people"}

PUT

/people/:id.:format

{:action=>"update", :controller=>"people"}

DELETE

/people/:id

{:action=>"destroy", :controller=>"people"}

DELETE

/people/:id.:format

{:action=>"destroy", :controller=>"people"}

For all of the routes that use HTTP GET methods, Rails creates a named route. As discussed later in the chapter, you can use these to support _path and _url helper methods with link_to and all of the other methods that need a path or URL for linking.

Note

If your application contains Ruby singleton objects, you should use map.resource rather than map.resources for its routing. It does most of the same work, but supporting a single object rather than a set. (Singleton objects have an include Singleton declaration in their class file, which marks it as deliberately allowing only one object of that kind in the application.)

This map.resources call, its 14 routes, and the supporting seven controller methods are all it takes to support the scaffolding. However, there will likely be times when you want to add an extra method to do something specific. You can do that without disrupting the existing RESTful methods by using the :member or :collection options. :member lets you specify actions that apply to individual resources, whereas :collection lets you specify actions that apply to a set of resources. For example, to add the roll method to the courses resource, Chapter 9 called:

map.resources :courses, :member => { :roll => :get }

In addition to the 14 methods, the routing now supports two extra. The named routes roll_course and the formatted_roll_course both use the GET method, as the parameter suggests. Both call the roll method on the courses controller, which you’ll have to create. The formatted_roll_course route adds a formatting parameter if one was provided.

If you need multiple extra methods, you just list them in the :member options hash:

map.resources :courses, :member => { :roll => :get, :history => :get,
:student_attendance => :get }

Nesting Resources

Chapter 9 went into extended detail on the many steps necessary to create an application using RESTful nested resources, in which only awards that applied to a given student were visible. Making that change required a shift at many levels, but the change inside of the routing was relatively small. Instead of two routing declarations in routes.rb:

map.resources :awards
map.resources :students

there was only one, combining them:

map.resources :students, :has_many => :awards

The resulting routes still create 14 routes for :awards, but they all look a little different. Instead of names such as award and new_award, they shift to student_award and new_student_award, highlighting their nested status. Their paths are all prefixed with /student/:student_id, as the award-specific parts of their URIs will appear after that, “below” students in the URI hierarchy.

Note

The declaration map.resources :students, :has_many => :awards is actually an abbreviated form, short for the more verbose:

do |students|
  students.resources :awards
end

If you need to add extra methods to the :awards resource, you’ll need to use this longer form, as shown in Chapter 17.

You can also specify multiple resources to nest by giving :has_many an array as its argument. If students also have, say, pets, you could make that a nested resource as well in a single declaration:

map.resources :students, :has_many => [:awards, :pets]

Note

If you’re nesting a has_one relationship instead of (or in addition to) a has_many, just use a has_one parameter on map.resources.

Checking the Map

As your list of routes grows, and especially as you get into some of the more complicated routing approaches, you may want to ask Rails exactly what it thinks the current routes are. The simplest way to do this is to use the rake routes command. Sometimes its results won’t be a big surprise, as when you run it on a new application with only the default routes:

/:controller/:action/:id
/:controller/:action/:id.:format

If you run it on a more complicated application, one with resources, you’ll get back a lot more detail—names of routes, methods, match strings, and parameters:

              students GET    /students
{:action=>"index", :controller=>"students"}
    formatted_students GET    /students.:format
{:action=>"index", :controller=>"students"}
                       POST   /students
{:action=>"create", :controller=>"students"}
                       POST   /students.:format
{:action=>"create", :controller=>"students"}
           new_student GET    /students/new
{:action=>"new", :controller=>"students"}
 formatted_new_student GET    /students/new.:format
{:action=>"new", :controller=>"students"}
          edit_student GET    /students/:id/edit
{:action=>"edit", :controller=>"students"}
formatted_edit_student GET    /students/:id/edit.:format
{:action=>"edit", :controller=>"students"}
               student GET    /students/:id 
{:action=>"show", :controller=>"students"}
     formatted_student GET    /students/:id.:format
{:action=>"show", :controller=>"students"}
                       PUT    /students/:id 
{:action=>"update", :controller=>"students"}
                       PUT    /students/:id.:format
{:action=>"update", :controller=>"students"}
                       DELETE /students/:id 
{:action=>"destroy", :controller=>"students"}
                       DELETE /students/:id.:format
{:action=>"destroy", :controller=>"students"}
...

And that’s just for one resource! Note that Rails lines these routes up on the HTTP method being called, which is not always the easiest way to read it. If you have lots of routes, and especially lots of resources, you’ll need some good search facilities to find what you’re looking for.

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

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