The Rails 4 Way (2014)
Chapter 2. Routing
I dreamed a thousand new paths. . . I woke and walked my old one.
—Chinese proverb
The routing system in Rails is the system that examines the URL of an incoming request and determines what action should be taken by the application. And it does a good bit more than that. Rails routing can be a bit of a tough nut to crack. But it turns out that most of the toughness resides in a small number of concepts. After you’ve got a handle on those, the rest falls into place nicely.
This chapter will introduce you to the principal techniques for defining and manipulating routes. The next chapter will build on this knowledge to explore the facilities Rails offers in support of writing applications that comply with the principles of Representational State Transfer (REST). As you’ll see, those facilities can be of tremendous use to you even if you’re not planning to scale the heights of REST theorization. Both chapters assume at least a basic knowledge of the Model-View-Controller (MVC) pattern and Rails controllers.
Some of the examples in these two chapters are based on a small auction application. The examples are kept simple enough that they should be comprehensible on their own. The basic idea is that there are auctions and each auction involves auctioning off an item. There are users and they submit bids. That’s it.
The triggering of a controller action is the main event in the life cycle of a connection to a Rails application. So it makes sense that the process by which Rails determines which controller and which action to execute must be very important. That process is embodied in the routing system.
The routing system maps URLs to actions. It does this by applying rules that you specify using a special syntax in the config/routes.rb file. Actually it’s just plain Ruby code, but it uses special methods and parameters, a technique sometimes referred to as an internal Domain Specific Language (DSL). If you’re using Rails generators, code gets added to the routes file automatically, and you’ll get some reasonable behavior. But it doesn’t take much work to write custom rules and reap the benefits of the flexibility of the routing system.
2.1 The Two Purposes of Routing
The routing system does two things: It maps requests to controller action methods, and it enables the dynamic generation of URLs for you for use as arguments to methods like link_to and redirect_to.
Each rule—or to use the more common term, route—specifies a pattern, which will be used both as a template for matching URLs and as a blueprint for creating them. The pattern can be generated automatically based on conventions, such as in the case of REST resources. Patterns can also contain a mixture of static substrings, forward slashes (mimicking URL syntax), and positional segment key parameters that serve as “receptors” for corresponding values in URLs.
A route can also include one or more hardcoded segment keys, in form of key/value pairs accessible to controller actions in a hash via the params method. A couple of keys (:controller and :action) determine which controller and action gets invoked. Other keys present in the route definition simply get stashed for reference purposes.
Putting some flesh on the bones of this description, here’s a sample route:
get 'recipes/:ingredient' => "recipes#index"
In this example, you find:
· static string (recipes)
· slash (/)
· segment key (:ingredient)
· controller action mapping ("recipes#index")
· HTTP verb constraining method (get)
Routes have a pretty rich syntax—this one isn’t by any means the most complex (nor the most simple)—because they have to do so much. A single route, like the one in this example, has to provide enough information both to match an existing URL and to manufacture a new one. The route syntax is engineered to address both of these processes.
2.2 The routes.rb File
Routes are defined in the file config/routes.rb, as shown (with some explanatory comments) in Listing 2.1. This file is created when you first create your Rails application and contains instructions about how to use it.
Listing 2.1: The default routes.rb file
1 Rails.application.routes.draw do
2 # The priority is based upon order of creation:
3 # first created -> highest priority.
4 # See how all your routes lay out with "rake routes".
5
6 # You can have the root of your site routed with "root"
7 # root 'welcome#index'
8
9 # Example of regular route:
10 # get 'products/:id' => 'catalog#view'
11
12 # Example of named route that can be invoked with
13 # purchase_url(id: product.id)
14 # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase
15
16 # Example resource route (maps HTTP verbs to controller
17 # actions automatically):
18 # resources :products
19
20 # Example resource route with options:
21 # resources :products do
22 # member do
23 # get 'short'
24 # post 'toggle'
25 # end
26 #
27 # collection do
28 # get 'sold'
29 # end
30 # end
31
32 # Example resource route with sub-resources:
33 # resources :products do
34 # resources :comments, :sales
35 # resource :seller
36 # end
37
38 # Example resource route with more complex sub-resources:
39 # resources :products do
40 # resources :comments
41 # resources :sales do
42 # get 'recent', on: :collection
43 # end
44 # end
45
46 # Example resource route with concerns:
47 # concern :toggleable do
48 # post 'toggle'
49 # end
50 # resources :posts, concerns: :toggleable
51 # resources :photos, concerns: :toggleable
52
53 # Example resource route within a namespace:
54 # namespace :admin do
55 # # Directs /admin/products/* to Admin::ProductsController
56 # # (app/controllers/admin/products_controller.rb)
57 # resources :products
58 # end
59 end
The whole file consists of a single call to the method draw of Rails.application.routes. That method takes a block, and everything from the second line of the file to the second-to-last line is the body of that block.
At runtime, the block is evaluated inside of an instance of the class ActionDispatch::Routing::Mapper. Through it you configure the Rails routing system.
The routing system has to find a pattern match for a URL it’s trying to recognize or a parameters match for a URL it’s trying to generate. It does this by going through the routes in the order in which they’re defined; that is, the order in which they appear in routes.rb. If a given route fails to match, the matching routine falls through to the next one. As soon as any route succeeds in providing the necessary match, the search ends.
2.2.1 Regular Routes
The basic way to define a route is to supply a URL pattern plus a controller class/action method mapping string with the special :to parameter.
get 'products/:id', to: 'products#show'
Since this is so common, a shorthand form is provided:
get 'products/:id' => 'products#show'
David has publicly commented on the design decision behind the shorthand form, when he said that it drew inspiration from two sources: 6
1) the pattern we’ve been using in Rails since the beginning of referencing controllers as lowercase without the “Controller” part in controller: "main" declarations and 2) the Ruby pattern of signaling that you’re talking about an instance method by using #. The influences are even part mixed. Main #index would be more confusing in my mind because it would hint that an object called Main actually existed, which it doesn’t. MainController#index would just be a hassle to type out every time. Exactly the same reason we went with controller: "main" vs controller: "MainController". Given these constraints, I think "main#index" is by far the best alternative…
2.2.2 Constraining Request Methods
As of Rails 4, it’s recommended to limit the HTTP method used to access a route. If you are using the match directive to define a route, you accomplish this by using the :via option:
match 'products/:id' => 'products#show', via: :get
Rails provides a shorthand way of expressing this particular constraint, by replacing match with the desired HTTP method (get, post, patch, etc.)
get 'products/:id' => 'products#show'
post 'products' => 'products#create'
If, for some reason, you want to constrain a route to more than one HTTP method, you can pass :via an array of verb names.
match 'products/:id' => 'products#show', via: [:get, :post]
Defining a route without specifying an HTTP method will result in Rails raising a RuntimeError exception. While strongly not recommended, a route can still match any HTTP method by passing :any to the :via option.
match 'products' => 'products#index', via: :any
2.2.3 URL Patterns
Keep in mind that there’s no necessary correspondence between the number of fields in the pattern string, the number of segment keys, and the fact that every connection needs a controller and an action. For example, you could write a route like
get ":id" => "products#show"
which would recognize a URL like
http://localhost:3000/8
The routing system would set params[:id] to 8 (based on the position of the :id segment key, which matches the position of 8 in the URL), and it would execute the show action of the products controller. Of course, this is a bit of a stingy route, in terms of visual information. On the other hand, the following example route contains a static string, products/, inside the URL pattern:
match 'products/:id' => 'products#show'
This string anchors the recognition process. Any URL that does not contain the static string products/ in its leftmost slot will not match this route.
As for URL generation, static strings in the route simply get placed within the URL that the routing system generates. The URL generator uses the route’s pattern string as the blueprint for the URL it generated. The pattern string stipulates the substring products.
As we go, you should keep the dual purpose of recognition/generation in mind, which is why it was mentioned several times so far. There are two principles that are particularly useful to remember:
· The same rule governs both recognition and generation. The whole system is set up so that you don’t have to write rules twice. You write each rule once, and the logic flows through it in both directions.
· The URLs that are generated by the routing system (via link_to and friends) only make sense to the routing system. The resulting URL http://example.com/products/19201, contains not a shred of a clue as to what’s supposed to happen when a user follows it—except insofar as it maps to a routing rule. The routing rule then provides the necessary information to trigger a controller action. Someone looking at the URL without knowing the routing rules won’t know which controller and action the URL maps to.
2.2.4 Segment Keys
The URL pattern string can contain parameters (denoted with a colon) and referred to as segment keys. In the following route declaration, :id is a segment key.
get 'products/:id' => 'products#show'
When this route matches a request URL, the :id portion of the pattern acts as a type of matcher, and picks up the value of that segment. For instance, using the same example, the value of id for the following URL would be 4: http://example.com/products/4
This route, when matched, will always take the visitor to the product controller’s show action. You’ll see techniques for matching controller and action based on segments of the URL shortly. The symbol :id inside the quoted pattern in the route is a segment key (that you can think of as a type of variable). Its job is to be latched onto by a value.
What that means in the example is that the value of params[:id] will be set to the string "4". You can access that value inside your products/show action.
When you generate a URL, you have to supply values that will attach to the segment keys inside the URL pattern string. The simplest to understand (and original) way to do that is using a hash, like this:
link_to "Products",
controller: "products",
action: "show",
id: 1
As you probably know, it’s actually more common nowadays to generate URLs using what are called named routes, versus supplying the controller and action parameters explicitly in a hash. However, right now we’re reviewing the basics of routing.
In the call to link_to, we’ve provided values for all three parameters of the route. Two of them are going to match the hard-coded, segment keys in the route; the third, :id, will be assigned to the corresponding segment key in the URL pattern.
It’s vital to understand that the call to link_to doesn’t know whether it’s supplying hard-coded or segment values. It just knows (or hopes!) that these three values, tied to these three keys, will suffice to pinpoint a route and therefore a pattern string, and therefore a blueprint for generating a URL dynamically.
Hardcoded Parameters It’s always possible to insert additional hardcoded parameters into route definitions that don’t have an effect on URL matching, but are passed along with the normal expected params. get 'products/special' => 'products#show', special: 'true' Mind you, I’m not suggesting that this example is a good practice. It would make more sense to me (as a matter of style) to point at a different action rather than inserting a clause. Your mileage may vary. get 'products/special' => 'products#special' |
2.2.5 Spotlight on the :id Field
Note that the treatment of the :id field in the URL is not magic; it’s just treated as a value with a name. If you wanted to, you could change the rule so that :id was :blah but then you’d have to do the following in your controller action:
@product = Product.find(params[:blah])
The name :id is simply a convention. It reflects the commonness of the case in which a given action needs access to a particular database record. The main business of the router is to determine the controller and action that will be executed.
The id field ends up in the params hash, already mentioned. In the common, classic case, you’d use the value provided to dig a record out of the database:
1 classProductsController < ApplicationController
2 def show
3 @product = Product.find(params[:id])
4 end
5 end
2.2.6 Optional Segment Keys
Rails 3 introduced a syntax for defining optional parts of the URL pattern. The easiest way to illustrate this syntax is by taking a look at the legacy default controller route, found in the previous versions of Rails at the bottom of a default config/routes.rb file:
match ':controller(/:action(/:id(.:format)))', via: :any
Note that parentheses are used to define optional segment keys, kind of like what you would expect to see when defining optional groups in a regular expression.
2.2.7 Redirect Routes
It’s possible to code a redirect directly into a route definition, using the redirect method:
get "/foo", to: redirect('/bar')
The argument to redirect can contain either a relative URL or a full URI.
get "/google", to: redirect('https://google.com/')
The redirect method can also take a block, which receives the request params as its argument. This allows you to, for instance, do quick versioning of web service API endpoints.7
actions in Rails 3 routes at http://yehudakatz.com/2009/12/20/generic-actions-in-rails-3/
match "/api/v1/:api",
to: redirect { |params| "/api/v2/#{params[:api].pluralize}" },
via: :any
The redirect method also accepts an optional :status parameter.
match "/api/v1/:api", to:
redirect(status: 302) { |params| "/api/v2/#{params[:api].pluralize}" },
via: :any
The redirect method returns an instance of ActionDispatch::Routing::Redirect, which is a simple Rack endpoint, as we can see by examining its source code.
1 moduleActionDispatch
2 moduleRouting
3 classRedirect # :nodoc:
4 ...
5 def call(env)
6 req = Request.new(env)
7
8 # If any of the path parameters has a invalid encoding then
9 # raise since it's likely to trigger errors further on.
10 req.symbolized_path_parameters.each do |key, value|
11 unless value.valid_encoding?
12 raise ActionController::BadRequest,
13 "Invalid parameter: #{key} => #{value}"
14 end
15 end
16
17 uri = URI.parse(path(req.symbolized_path_parameters, req))
18 uri.scheme ||= req.scheme
19 uri.host ||= req.host
20 uri.port ||= req.port unless req.standard_port?
21
22 if relative_path?(uri.path)
23 uri.path = "#{req.script_name}/#{uri.path}"
24 end
25
26 body = %(<html><body>You are being
27 <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)
28
29 headers = {
30 'Location' => uri.to_s,
31 'Content-Type' => 'text/html',
32 'Content-Length' => body.length.to_s
33 }
34
35 [ status, headers, [body] ]
36 end
37 ...
38 end
39 end
40 end
2.2.8 The Format Segment
Let’s revisit the legacy default route again:
match ':controller(/:action(/:id(.:format)))', via: :any
The .:format at the end matches a literal dot and a “format” segment key after the id field. That means it will match, for example, a URL like:
http://localhost:3000/products/show/3.json
Here, params[:format] will be set to json. The :format field is special; it has an effect inside the controller action. That effect is related to a method called respond_to.
The respond_to method allows you to write your action so that it will return different results, depending on the requested format. Here’s a show action for the products controller that offers either HTML or JSON:
1 def show
2 @product = Product.find(params[:id])
3 respond_to do |format|
4 format.html
5 format.json { render json: @product.to_json }
6 end
7 end
The respond_to block in this example has two clauses. The HTML clause just consists of format.html. A request for HTML will be handled by the usual rendering of a view template. The JSON clause includes a code block; if JSON is requested, the block will be executed and the result of its execution will be returned to the client.
Here’s a command-line illustration, using curl (slightly edited to reduce line noise):
$ curl http://localhost:3000/products/show/1.json -i
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 81
Connection: Keep-Alive
{"created_at":"2013-02-09T18:25:03.513Z",
"description":"Keyboard",
"id":"1",
"maker":"Apple",
"updated_at":"2013-02-09T18:25:03.513Z"}
The .json on the end of the URL results in respond_to choosing the json branch, and the returned document is an JSON representation of the product.
Requesting a format that is not included as an option in the respond_to block will not generate an exception. Rails will return a 406 Not Acceptable status, to indicate that it can’t handle the request.
If you want to setup an else condition for your respond_to block, you can use the any method, which tells Rails to catch any other formats not explicitly defined.
1 def show
2 @product = Product.find(params[:id])
3 respond_to do |format|
4 format.html
5 format.json { render json: @product.to_json }
6 format.any
7 end
8 end
Just make sure that you explicitly tell any what to do with the request or have view templates corresponding to the formats you expect. Otherwise, you’ll get a MissingTemplate exception.
ActionView::MissingTemplate (Missing template products/show,
application/show with {:locale=>[:en], :formats=>[:xml],
:handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}.)
2.2.9 Routes as Rack Endpoints
You’ll see usage of the :to option in routes throughout this chapter. What’s most interesting about :to is that its value is what’s referred to as a Rack Endpoint. To illustrate, consider the following simple example:
get "/hello", to: proc { |env| [200, {}, ["Hello world"]] }
The router is very loosely coupled to controllers! The shorthand syntax (like "items#show") relies on the action method of controller classes to return a Rack endpoint that executes the action requested.
>> ItemsController.action(:show)
=> #<Proc:0x01e96cd0@...>
The ability to dispatch to a Rack-based application, such as one created with Sinatra, can be achieved using the mount method. The mount method accepts an :at option, which specifies the route the Rack-based application will map to.
1 classHelloApp < Sinatra::Base
2 get "/" do
3 "Hello World!"
4 end
5 end
6
7 Rails.application.routes.draw do
8 mount HelloApp, at: '/hello'
9 end
Alternatively, a shorthand form is also available:
mount HelloApp => '/hello'
2.2.10 Accept Header
You can also trigger a branching on respond_to by setting the Accept header in the request. When you do this, there’s no need to add the .:format part of the URL. (However, note that out in the real world, it’s difficult to get this technique to work reliably due to HTTP client/browser inconsistencies.)
Here’s a curl example that does not specify an .json format, but does set the Accept header to application/json:
$ curl -i -H "Accept: application/json"
http://localhost:3000/products/show/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 81
Connection: Keep-Alive
{"created_at":"2013-02-09T18:25:03.513Z",
"description":"Keyboard",
"id":"1",
"maker":"Apple",
"updated_at":"2013-02-09T18:25:03.513Z"}
The result is exactly the same as in the previous example.
2.2.11 Segment Key Constraints
Sometimes you want not only to recognize a route, but to recognize it at a finer-grained level than just what components or fields it has. You can do this through the use of the :constraint option (and possibly regular expressions).
For example, you could route all show requests so that they went to an error action if their id fields were non-numerical. You’d do this by creating two routes, one that handled numerical ids, and a fall-through route that handled the rest:
get ':controller/show/:id' => :show, constraints: {:id => /\d+/}
get ':controller/show/:id' => :show_error
Implicit Anchoring The example constraint we’ve been using constraints: {:id => /\d+/} seems like it would match "foo32bar". It doesn’t because Rails implicitly anchors it at both ends. In fact, as of this writing, adding explicit anchors \A and \z causes exceptions to be raised. |
Apparently, it’s so common to set constraints on the :id param, that Rails lets you shorten our previous example to simply
get ':controller/show/:id' => :show, id: /\d+/
get ':controller/show/:id' => :show_error
Regular expressions in routes can be useful, especially when you have routes that differ from each other only with respect to the patterns of their components. But they’re not a full-blown substitute for data-integrity checking. You probably still want to make sure that the values you’re dealing with are usable and appropriate for your application’s domain.
From the example, you might conclude that :constraints checking applies to elements of the params hash. However, you can also check a grab-bag of other request attributes that return a string, such as :subdomain and :referrer. Matching methods of request that return numeric or boolean values are unsupported and will raise a somewhat cryptic exception during route matching.
# only allow users admin subdomain to do old-school routing
get ':controller/:action/:id' => :show, constraints: {subdomain: 'admin'}
If for some reason you need more powerful constraints checking, you have full access to the request object, by passing a block or any other object that responds to call as the value of :constraints like:
# protect records with id under 100
get 'records/:id' => "records#protected",
constraints: proc { |req| req.params[:id].to_i < 100 }
2.2.12 The Root Route
At around line 8 of the default config/routes.rb (refer to Listing 2.1) you’ll see
# You can have the root of your site routed with "root"
# root 'welcome#index'
What you’re seeing here is the root route, that is, a rule specifying what should happen when someone connects to
http://example.com # Note the lack of "/anything" at the end!
The root route says, “I don’t want any values; I want nothing, and I already know what controller and action I’m going to trigger!”
In a newly generated routes.rb file, the root route is commented out, because there’s no universal or reasonable default for it. You need to decide what this nothing URL should do for each application you write.
Here are some examples of fairly common empty route rules:
1 root to: "welcome#index"
2 root to: "pages#home"
3
4 # Shorthand syntax
5 root "user_sessions#new"
Defining the empty route gives people something to look at when they connect to your site with nothing but the domain name. You might be wondering why you see something when you view a newly-generated Rails application, that still has its root route commented out.
The answer is that if a root route is not defined, by default Rails will route to an internal controller Rails::WelcomeController and render a welcome page instead.
In previous versions of Rails, this was accomplished by including the file index.html in the public directory of newly generated applications. Any static content in the public directory hierarchy, matching the URL scheme that you come up with for your app, results in the static content being served up instead of triggering the routing rules. Actually, the web server will serve up the content without involving Rails at all.
A Note on Route Order Routes are consulted, both for recognition and for generation, in the order they are defined in routes.rb. The search for a match ends when the first match is found, meaning that you have to watch out for false positives. |
2.3 Route Globbing
In some situations, you might want to grab one or more components of a route without having to match them one by one to specific positional parameters. For example, your URLs might reflect a directory structure. If someone connects to
/items/list/base/books/fiction/dickens
you want the items/list action to have access to all four remaining fields. But sometimes there might be only three fields:
/items/list/base/books/fiction
or five:
/items/list/base/books/fiction/dickens/little_dorrit
So you need a route that will match (in this particular case) everything after the second URI component. You define it by globbing the route with an asterisk.
get 'items/list/*specs', controller: 'items', action: 'list'
Now, the products/list action will have access to a variable number of slash-delimited URL fields, accessible via params[:specs]:
def list
specs = params[:specs] # e.g, "base/books/fiction/dickens"
end
Globbing Key-Value Pairs Route globbing might provide the basis for a general mechanism for fielding ad hoc queries. Let’s say you devise a URI scheme that takes the following form: http://localhost:3000/items/q/field1/value1/field2/value2/... Making requests in this way will return a list of all products whose fields match the values, based on an unlimited set of pairs in the URL. In other words, http://localhost:3000/items/q/year/1939/material/wood could generate a list of all wood items made in 1939. The route that would accomplish this would be: get 'items/q/*specs', controller: "items", action: "query" Of course, you’ll have to write a query action like this one to support the route: 1 def query 2 @items = Item.where(Hash[*params[:specs].split("/")]) 3 if @items.empty? 4 flash[:error] = "Can't find items with those properties" 5 end 6 render :index 7 end How about that square brackets class method on Hash, eh? It converts a one-dimensional array of key/value pairs into a hash! Further proof that in-depth knowledge of Ruby is a prerequisite for becoming an expert Rails developer. |
2.4 Named Routes
The topic of named routes almost deserves a chapter of its own. In fact, what you learn here will feed directly into our examination of REST-related routing in Chapter 3.
The idea of naming a route is basically to make life easier on you, the programmer. There are no outwardly visible effects as far as the application is concerned. When you name a route, a new method gets defined for use in your controllers and views; the method is called name_url (with name being the name you gave the route), and calling the method, with appropriate arguments, results in a URL being generated for the route. In addition, a method called name_path also gets created; this method generates just the path part of the URL, without the protocol and host components.
2.4.1 Creating a Named Route
The way you name a route is by using the optional :as parameter in a rule:
get 'help' => 'help#index', as: 'help'
In this example, you’ll get methods called help_url and help_path, which you can use wherever Rails expects a URL or URL components:
link_to "Help", help_path
And, of course, the usual recognition and generation rules are in effect. The pattern string consists of just the static string component "help". Therefore, the path you’ll see in the hyperlink will be
/help
When someone clicks on the link, the index action of the help controller will be invoked.
Xavier says… You can test named routes in the console directly using the special app object. >> app.clients_path => "/clients" >> app.clients_url => "http://www.example.com/clients" |
Named routes save you some effort when you need a URL generated. A named route zeros in directly on the route you need, bypassing the matching process that would be needed other. That means you don’t have to provide as much detail as you otherwise would, but you still have to provide values for any segment keys in the route’s pattern string that cannot be inferred.
2.4.2 name_path vs. name_url
When you create a named route, you’re actually creating at least two route helper methods. In the preceding example, those two methods are help_url and help_path. The difference is that the _url method generates an entire URL, including protocol and domain, whereas the _path method generates just the path part (sometimes referred to as an absolute path or a relative URL).
According to the HTTP spec, redirects should specify a URI, which can be interpreted (by some people) to mean a fully-qualified URL. Therefore, if you want to be pedantic about it, you probably should always use the _url version when you use a named route as an argument to redirect_toin your controller code.
The redirect_to method works perfectly with the relative URLs generated by _path helpers, making arguments about the matter kind of pointless. In fact, other than redirects, permalinks, and a handful of other edge cases, it’s the Rails way to use _path instead of _url. It produces a shorter string and the user agent (browser or otherwise) should be able to infer the fully qualified URL whenever it needs to do so, based on the HTTP headers of the request, a base element in the document, or the URL of the request.
As you read this book and as you examine other code and other examples, the main thing to remember is that help_url and help_path are basically doing the same thing. I tend to use the _url style in general discussions about named route techniques, but to use _path in examples that occur inside view templates (for example, with link_to and form_for). It’s mostly a writing-style thing, based on the theory that the URL version is more general and the path version more specialized. In any case, it’s good to get used to seeing both and getting your brain to view them as very closely connected.
Using Literal URLs You can, if you wish, hard-code your paths and URLs as string arguments to link_to, redirect_to, and friends. For example, instead of link_to "Help", controller: "main", action: "help" you can write link_to "Help", "/main/help" However, using a literal path or URL bypasses the routing system. If you write literal URLs, you’re on your own to maintain them. (You can of course use Ruby’s string interpolation techniques to insert values, if that’s appropriate for what you’re doing, but really stop and think about whether you are reinventing Rails functionality if you go down that path.) |
2.4.3 What to Name Your Routes
As we’ll learn in Chapter 3, the best way to figure out what names you should use for your routes is to follow REST conventions, which are baked into Rails and simplify things greatly. Otherwise, you’ll need to think top-down; that is, think about what you want to write in your application code, and then create the routes that will make it possible.
Take, for example, this call to link_to
link_to "Auction of #{item.name}",
controller: "items",
action: "show",
id: item.id
The routing rule to match that path is (a generic route):
get "item/:id" => "items#show"
It sure would be nice to shorten that link_to code. After all, the routing rule already specifies the controller and action. This is a good candidate for a named route for items:
get "item/:id" => "items#show", as: "item"
Lets improve the situation by introducing item_path in the call to link_to:
link_to "Auction of #{item.name}", item_path(id: item.id)
Giving the route a name is a shortcut; it takes us straight to that route, without a long search and without having to provide a thick description of the route’s hard-coded parameters.
2.4.4 Argument Sugar
In fact, we can make the argument to item_path even shorter. If you need to supply an id number as an argument to a named route, you can just supply the number, without spelling out the :id key:
link_to "Auction of #{item.name}", item_path(item.id)
And the syntactic sugar goes even further: You can and should provide objects and Rails will grab the id automatically.
link_to "Auction of #{item.name}", item_path(item)
This principle extends to other segment keys in the pattern string of the named route. For example, if you’ve got a route like
get "auction/:auction_id/item/:id" => "items#show", as: "item"
you’d be able to call it like
link_to "Auction of #{item.name}", item_path(auction, item)
and you’d get something like this as your path (depending on the exact id numbers):
/auction/5/item/11
Here, we’re letting Rails infer the ids of both an auction object and an item object, which it does by calling to_param on whatever non-hash arguments you pass into named route helpers. As long as you provide the arguments in the order in which their ids occur in the route’s pattern string, the correct values will be dropped into place in the generated path.
2.4.5 A Little More Sugar with Your Sugar?
Furthermore, it doesn’t have to be the id value that the route generator inserts into the URL. As alluded to a moment ago, you can override that value by defining a to_param method in your model.
Let’s say you want the description of an item to appear in the URL for the auction on that item. In the item.rb model file, you would override to_params; here, we’ll override it so that it provides a “munged” (stripped of punctuation and joined with hyphens) version of the description, courtesy of the parameterize method added to strings in Active Support.
1 def to_param
2 description.parameterize
3 end
Subsequently, the method call item_path(auction, item) will produce something like
/auction/3/item/cello-bow
Of course, if you’re putting things like “cello-bow” in a path field called :id, you will need to make provisions to dig the object out again. Blog applications that use this technique to create slugs for use in permanent links often have a separate database column to store the munged version of the title that serves as part of the path. That way, it’s possible to do something like
Item.where(munged_description: params[:id]).first!
to unearth the right item. (And yes, you can call it something other than :id in the route to make it clearer!)
Courtenay says…. Why shouldn’t you use numeric IDs in your URLs? First, your competitors can see just how many auctions you create. Numeric consecutive IDs also allow people to write automated spiders to steal your content. It’s a window into your database. And finally, words in URLs just look better. |
2.5 Scoping Routing Rules
Rails gives you a variety of ways to bundle together related routing rules concisely. They’re all based on usage of the scope method and its various shortcuts. For instance, let’s say that you want to define the following routes for auctions:
1 get 'auctions/new' => 'auctions#new'
2 get 'auctions/edit/:id' => 'auctions#edit'
3 post 'auctions/pause/:id' => 'auctions#pause'
You could DRY up your routes.rb file by using the scope method instead:
1 scope controller: :auctions do
2 get 'auctions/new' => :new
3 get 'auctions/edit/:id' => :edit
4 post 'auctions/pause/:id' => :pause
5 end
Then you would DRY it up again by adding the :path argument to scope:
1 scope path: '/auctions', controller: :auctions do
2 get 'new' => :new
3 get 'edit/:id' => :edit
4 post 'pause/:id' => :pause
5 end
2.5.1 Controller
The scope method accepts a :controller option (or it can interpret a symbol as its first argument to assume a controller). Therefore, the following two scope definitions are identical:
scope controller: :auctions do
scope :auctions do
To make it more obvious what’s going on, you can use the controller method instead of scope, in what’s essentially syntactic sugar:
controller :auctions do
2.5.2 Path Prefix
The scope method accepts a :path option (or it can interpret a string as its first parameter to mean a path prefix). Therefore, the following two scope definitions are identical:
scope path: '/auctions' do
scope '/auctions' do
New to Rails 4, is the ability to pass the :path option symbols instead of strings. The following scope definition:
scope :auctions, :archived do
will scope all routes nested under it to the “/auctions/archived” path.
2.5.3 Name Prefix
The scope method also accepts a :as option that affects the way that named route URL helper methods are generated. The route
1 scope :auctions, as: 'admin' do
2 get 'new' => :new, as: 'new_auction'
3 end
will generate a named route URL helper method called admin_new_auction_url.
2.5.4 Namespaces
URLs can be grouped by using the namespace method, which is syntactic sugar that rolls up module, name prefix and path prefix settings into one declaration. The implementation of the namespace method converts its first argument into a string, which is why in some example code you’ll see it take a symbol.
1 namespace :auctions, :controller => :auctions do
2 get 'new' => :new
3 get 'edit/:id' => :edit
4 post 'pause/:id' => :pause
5 end
2.5.5 Bundling Constraints
If you find yourself repeating similar segment key constraints in related routes, you can bundle them together using the :constraints option of the scope method:
1 scope controller: :auctions, constraints: {:id => /\d+/} do
2 get 'edit/:id' => :edit
3 post 'pause/:id' => :pause
4 end
It’s likely that only a subset of rules in a given scope need constraints applied to them. In fact, routing will break if you apply a constraint to a rule that doesn’t take the segment keys specified. Since you’re nesting, you probably want to use the constraints method, which is just more syntactic sugar to tighten up the rule definitions.
1 scope path: '/auctions', controller: :auctions do
2 get 'new' => :new
3 constraints id: /\d+/ do
4 get 'edit/:id' => :edit
5 post 'pause/:id' => :pause
6 end
7 end
To enable modular reuse, you may supply the constraints method with an object that has a matches? method.
1 classDateFormatConstraint
2 defself.matches?(request)
3 request.params[:date] =~ /\A\d{4}-\d\d-\d\d\z/ # YYYY-MM-DD
4 end
5 end
6
7 # in routes.rb
8 constraints(DateFormatConstraint) do
9 get 'since/:date' => :since
10 end
In this particular example (DateFormatConstraint) if an errant or malicious user input a badly formatted date parameter via the URL, Rails will respond with a 404 status instead of causing an exception to be raised.
2.6 Listing Routes
A handy route listing utility is included in all Rails projects as a standard rake task. Invoke it by typing rake routes in your application directory. For example, here is the output for a routes file containing just a single resources :products rule:
$ rake routes
products GET /products(.:format) products#index
POST /products(.:format) products#create
new_product GET /products/new(.:format) products#new
edit_product GET /products/:id/edit(.:format) products#edit
product GET /products/:id(.:format) products#show
PATCH /products/:id(.:format) products#update
PUT /products/:id(.:format) products#update
DELETE /products/:id(.:format) products#destroy
The output is a table with four columns. The first two columns are optional and contain the name of the route and HTTP method constraint, if they are provided. The third column contains the URL mapping string. Finally, the fourth column indicates the controller and action method that the route maps to, plus constraints that have been defined on that routes segment keys (if any).
Note that the routes task checks for an optional CONTROLLER environment variable
$ rake routes CONTROLLER=products
would only lists the routes related to ProductsController.
Juanito says… While you have a server up and running on development environment, You could visit /rails/info/routes to get a complete list of routes of your Rails application. |
2.7 Conclusion
The first half of the chapter helped you to fully understand the generic routing rules of Rails and how the routing system has two purposes:
· Recognizing incoming requests and mapping them to a corresponding controller action, along with any additional variable receptors.
· Recognizing URL parameters in methods such as link_to and matching them up to a corresponding route so that proper HTML links can be generated.
We built on our knowledge of generic routing by covering some advanced techniques such as using regular expressions and globbing in our route definitions, plus the bundling of related routes under shared scope options.
Finally, before moving on, you should make sure that you understand how named routes work and why they make your life easier as a developer by allowing you to write more concise view code. As you’ll see in the next chapter, when once we start defining batches of related named routes, we’re on the cusp of delving into REST.