8.1. Defining Routes with map.connect

When you create a new application, Rails generates a config outes.rb file that defines two default (unnamed or anonymous) routes:

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

The routing subsystem will try to find a match between the defined patterns and the URL of the incoming request.

For example, take the first route into consideration:

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

This tells Rails that the route recognizes only paths that include an arbitrarily named controller, action, and id. Hence, '/library/borrow/25189' matches this route, and instructs Rails to map the first token in the URL (that is, library) to the :controller parameter, the second token (for example, borrow) to the :action parameter, and the third token to the :id parameter, which has a value of "25189." The params object will then look like this:

params = { :controller => "library", :action => "borrow", :id => "25189" }

Rails will therefore be able to process the request, by instantiating the library controller and executing the borrow action defined within it. As a developer, you'll be able to retrieve the :id parameter from within the controller through params[:id].

8.1.1. Default Parameters

/library/borrow/25189 results in the parameters { :controller => "library", :action=> "borrow", :id => "25189" }, but not all three parameters are strictly required.

You could omit an id, issuing perhaps a request for the path /library/catalog instead. The parameters would then be the following:

params = { :controller => "library", :action => "catalog" }

Such a match is possible, despite the missing third parameter, because connect defines a default for :id. Unless a value is specified in the URL, :id is nil. And :id => nil doesn't appear in the params hash-like object, because nil parameters that are not specified in the path are not included.

Similarly, connect defines a default value when the action is missing. Reasonably so, this default value is "index". /library will therefore be mapped to { :controller => "library", :action => "index" }.

Before scaffolding was refactored to become RESTful, the default routes and their default values still enabled beautiful paths such as /library, /library/list, and /library/show/12345 to work just as you'd expect them to. To this day, if you have a non-RESTful controller, the presence of these default routes in routes.rb will allow you to have paths in the format /controller_name, /controller_name/action_name, and /controller_name/action_name/id.

8.1.2. Customizing Your Routes

This default route is rather generic, because its pattern will be matched by requests whose URLs contain an arbitrary controller name (as long as the controller exists), an arbitrarily named action (whether or not it exists), and an arbitrary id. On top of that, any HTTP verb is fair play for this route. But don't let this mislead you into thinking that it would match any path. On the contrary, /library/borrow/43274/something or /library/something/borrow/43274 will not match this route, because they both have four tokens, rather than the three expected.

The :id parameter can have any value, and it's not limited to numeric values enclosed in a string, although this is often the case because params[:id] is usually passed to ActiveRecord::Base's find method.

:controller and :action are specially named parameters because they are used to identify the controller and the action that should process the request, respectively. Any other symbol within the pattern, even :id, will, however, be considered as a regular named parameter and included in the params object. :controller and :action can be used anywhere you need to match a controller or action name; it's also possible to explicitly require a specific controller, and/or action, by passing an option (for example, :controller or :action) to the connect method. For example, consider the following route declaration:

map.connect 'library/:action/:isbn', :controller => "library"

/library/borrow/9780470189481 will then be mapped as follows:

params = { :controller=> "library", :action=> "borrow", :isbn=> "9780470189481" }

The route will also match the path /library/archive/9780470189481. In fact, this will yield:

params = { :controller=> "library", :action=> "archive", :isbn=> "9780470189481" }

The method with_options can be used to declare several conditions that are common to a series of routes defined within the block passed to the method. Check the online documentation for examples.

Notice how the action is "variable," and can be arbitrary because you use the :action symbol in the pattern. However, the controller is not variable, so for the route to match, the path requested by the user must begin with /library/ and be followed by an action and an ISBN. :id defaults to nil but your custom named parameter :isbn doesn't. This means that a value must be provided or you won't have a match. Consequently, you can't just have /library/9780470189481 either, because 9780470189481 would be interpreted as the value that's supposed to be assigned to :action and the :isbn value would still be missing in Rails' eyes.

There are times when this is exactly what you want: a mandatory set of parameters that cannot be omitted for a certain route. For example, the previously defined route doesn't work well if you want to map actions like borrow and archive in LibraryController, assuming as you can imagine neither of these actions would be meaningful without access to an :isbn parameter that identifies a certain book.

Here archive is intended as the action of "returning a book" to the archive, as opposed to listing the catalog, which wouldn't require an :isbn parameter.

It is, however, possible to define your own default values through the :defaults option. To make :isbn an optional parameter, for example, you could do the following:

map.connect 'library/:action/:isbn', :controller => "library",
                                     :defaults => { :isbn => nil }

If you omit the ISBN, it will simply not appear in the parameters hash. And because you removed this constraint, you can now also omit an action (for example, "/library") and fall back on :action's default value, which is "index."

Route Globbing

Ruby methods can have a variable number of arguments thanks to the splat operator that we encountered in the Ruby section of this book. You might recall that in the method signature, a parameter that's prefixed by a splat operator has to be the last one in the signature.

Within the pattern of a route, you can do something similar in an attempt to catch a variable number of slash-separated tokens provided in the URL. If you place *path (or any other name, really) in the pattern, you'll be able to catch a variable quantity of tokens in the URL through params[:path] (assuming you used *path).

This is known as "route globbing" and unlike regular Ruby methods, the glob doesn't have to be the last parameter in the pattern.

For example, if the pattern is 'library/:action/:isbn/*extra/preview' and the path requested by the user is '/library/borrow/9780470189481/3/weeks/preview,' not only will there be a match between the path and the pattern specified for this route, but you'll also be able to access those additional parameters through params[:extra], which will contain the array ["3", "weeks"].


The first parameter passed to connect already gives you a great deal of flexibility, especially if you consider that you can have an arbitrary number of defined routes and can therefore fine tune which URLs are handled by which route. Declaring routes is, however, even more customizable thanks to the fact that connect accepts a second parameter, a hash of options. Let's review all the accepted keys for that option hash:

  • :action: Used to indicate what action should handle the request when there is a match between the requested URL and the pattern specified by the route declaration. If :action is present within the pattern, this has precedence over the :action option. This means that passing :action => "my_action" to connect will only have an effect if no action name has been indicated in the URL. In such a circumstance, :action is equivalent to adding an entry for the action to :defaults.

  • :conditions: Used to define restrictions on routes, it supports the :method condition that specifies which HTTP methods can access the route among :post, :get, :put, :delete, and the catch-all :any. This is particularly handy when, for example, you want to specify that a different action should handle the requested URL, depending on whether this is a GET or POST request. To handle that situation, you could specify two routes, and pass :action => "first_action", :conditions => { :method => :get } to the first route, and :action => "second_action", :conditions => { :method => :post } to the second route.

  • :controller: Similarly to :action, this is used to indicate which controller should be mapped to the route. If the catch-all :controller symbol appears within the pattern specified by the route, the :controller => "example" option will only apply when a controller is not provided in the URL. Just like in the case of :action, you have the option to use :defaults instead, if you'd like.

  • :defaults: Used to specify default values for one or more named parameters included in the pattern passed to connect. For instance, consider the following route declaration:

    map.connect 'library/:action/:isbn', :controller => 'library',
                                         :defaults => { :action => 'info',
                                                        :isbn => nil }

    This will map the path '/library' as requested by an end user, with the controller library and the action info (an :isbn key will not be added to params, because the :isbn named parameter was defaulted to nil).

  • :<parameter_name>: This option can be used for two different tasks depending on the assigned value. When a regular expression is assigned, this sets a requirement for the named parameter (for example, :quantity => /d+/). If the condition is not satisfied (for example, :quantity exists in the URL but its value is not composed entirely of digits), the route will not match the request. The second way of using this is to assign a regular value so that the parameter is added to the params object. Note that, unlike entries in the :defaults hash, parameters added in this way are not required to be named parameters within the pattern. In practice, this means that you can use :my_parameter => "my_value" anytime you need to associate a parameter with a certain request. The params object will enable you to retrieve that value from within the action (for example, with params[:my_parameter]).

  • :requirements: Used to specify constraints on the format of one or more parameters that appear in the URL. If any of the parameters specified in the request fail to satisfy the condition defined within the :requirements hash, the route will not apply. This is entirely equivalent to using multiple :<parameter_name> options whose assigned values are regular expressions. A typical example to illustrate the usefulness of :requirements is the permalink of blog engines. If the pattern used for the route is 'example/:year/:month/:day,' you'll be able to retrieve the three named parameters from within the controller through params[:year], params[:month] and params[:day]. However, this doesn't guarantee that the three values you received were properly formatted. It would be better to show a 404 page for requests whose URLs are meaningless, like '/example/1492/10/12' (unless you're transcribing Columbus' diary) or '/example/2008/03/50.' In that case, it will be sufficient to pass :requirements => { :year => /20dd/, :month => /(0?[1-9]|1[012])/, :day => /(0?[1-9]|[12]d|3[01])/ } to connect. If any of the three named parameters don't meet the specified conditions, a match between the incoming request and this route will not appear.

Note that the regular expressions in this case are just an aid to exclude the majority of incorrect dates and values. However, this doesn't take into account leap years or the fact that certain months don't have 31 days.

Each route that you define will typically have a pattern with a few named parameters in it (for example, :isbn), and a few options that are different from the ones we encountered earlier (for example, :defaults), in a manner that enables the developer to define with surgical precision how URLs should be recognized and mapped.

8.1.3. Route Priority

The block passed to ActionController::Routing::Routes.draw in route.rb can contain multiple routes, and it's also possible to have more than one route that matches the incoming request URL and HTTP verb. Yet, each request needs to be mapped with only one route, so that Rails unequivocally knows which controller, and action, should handle the request, and what parameters should be available to them.

Thankfully, routing solves this problem for you by giving a different priority to each route you define. Starting from the top and moving toward the bottom, the pattern-matching algorithm will check each route's pattern, conditions, and requirements against the request until it finds a match. When a matching route is located, no further routes are checked for that request. This implies that the routes within the block are presented in order of their priority. The first route will have the highest priority and the last route the lowest.

This is an important notion because you don't want the intended route for a given request to be obscured by another matching route that happens to have greater priority. This is why the default routes are placed toward the end of the block, rather than at the beginning. The default routes are so generic that they can be considered as "catch-all" routes (to a certain extent), when more specific routes do not apply. If no routes match the incoming request (not even the default ones), an ActionController::RoutingError exception is raised.

Take into consideration the routes.rb file defined for the simple blog application. If you were to change the order of the routes, and place the default routes on top, you'd have the following:

# Don't do this
ActionController::Routing::Routes.draw do |map|
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'


  map.root :controller => "articles"
  map.resources :articles, :has_many => :comments,
                           :collection => { :unpublished => :get }
end

When a RESTful request comes in, in trying to visualize an article at /articles/3/show, routing would try to match that with the first route. The pattern in the first route is now ':controller/:action/:id.' Do you have a match? Indeed, because the default route is so generic that it requires only three tokens in the URL and accepts any HTTP verb.

The end result would be that the request is mapped to the articles controller (correct), the action 3 (wrong), and the id show (wrong again).

It is in fact advisable to comment out the default routes altogether if they are not needed within a RESTful application. It is also a matter of preventing the existence of default routes that can be used to create or delete objects with simple GET requests. So please go ahead and delete or comment them out in the blog application you created, so that the routes.rb file appears as follows:

ActionController::Routing::Routes.draw do |map|
  map.root :controller => "articles"
  map.resources :articles, :has_many => :comments,
                           :collection => { :unpublished => :get }
end

NOTE

The order in which routes appear matters. Always place the most generic routes at the bottom. If yours is a RESTful application, it is advisable to comment out the default routes.

8.1.4. Routes from the Console

The console was an indispensible tool when dealing with ActiveRecord, and not surprisingly, it can be highly useful when working with controllers as well. Things are, however, less straightforward, so let's explore how you can work with routes from the console.

When working with routes, you're interested in two tasks. The first is to determine which route matches a given URL, so that you can see which controller, action, and parameters result from the request. The second is the exact opposite: you have a controller, action, and parameters and you need to obtain a URL.

All the routes that you define in routes.rb are added to Routes, which is a RouteSet object. Both the class and the object are defined in ActionController::Routing. You use Routes and its methods to both recognize a given path and generate URLs. Let's investigate the former first.

You need to start the console and assign ActionController::Routing::Routes to a local variable for the sake of convenience:

>> routes = ActionController::Routing::Routes

The output generated will be quite large because a lot of information is stored in this object.

Now that you have a handle to play with, you can use the recognize_path method.

Notice that for this to work, the controller needs to be defined in the application. If you are testing the routing for controllers you haven't defined yet, you can place their names in an array, and pass that array to use_controllers!:

>> ActionController::Routing::use_controllers! ["main", "library"]

As well, you should reload the routing file for good measure (that is, load 'config/routes.rb').

You can now recognize paths as follows:

>> routes.recognize_path '/main/show/3'
=> {:action => "show", :controller => "main", :id => "3"}

On the flip side, generating URLs can be accomplished through the generate method:

>> routes.generate :controller => 'main', :coupon => 1920321
=> "/main?coupon=1920321"

These are just examples, of course; the output depends entirely on the routes you define.

:coupon is a parameter that was not specified in the pattern for the route that applies, and as such it's appended to the URL.

Normally you'd use the method url_for in the controller (and the link_to helper in the view) to generate a URL for a certain controller, action, and its required parameters. For anonymous routes, a link in the controller might look like this:

@borrow_link = url_for(:controller => 'library', :action => 'borrow',
                                                 :isbn => @book.isbn)

Controllers can be defined within modules, and in such cases the specified controller is assumed to be local to the module that contains the controller that issues the request. Use /example to indicate that it's an absolute "path" to the controller.

This will assign a string like 'http://localhost:3000/library/borrow/9780307237699' to @borrow_link. The actual string will of course depend on the hostname, port (typically 80 in production), and :isbn parameter, and assumes that the default route was specified in the routes.rb file.

The url_for method is quite flexible; check the online documentation for a complete list and description of accepted options.

Note that you can't use url_for directly from the console, but that you have to resort to the analogous generate method. A similar method called url_for is available through the special object app, which is an instance of ActionController::Integration::Session, but it doesn't behave exactly like the url_for method in a controller does. For example, if it lacks an associated request, the generated URL will use www.example.com as the hostname.

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

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