Home > Articles > Programming > Ruby

  • Print
  • + Share This
This chapter is from the book

This chapter is from the book

3.8 RESTful Route Customizations

Rails's RESTful routes give you a pretty nice package of named routes, mapped to useful, common, controller actions—the CRUD superset you've already learned about. Sometimes, however, you want to customize things a little more, while still taking advantage of the RESTful route naming conventions and the multiplication table approach to mixing named routes and HTTP request methods.

The techniques for doing this are useful when, for example, you've got more than one way of viewing a resource that might be described as showing. You can't (or shouldn't) use the show action itself for more than one such view. Instead, you need to think in terms of different perspectives on a resource, and create URLs for each one.

3.8.1 Extra Member Routes

For example, let's say we want to make it possible to retract a bid. The basic nested route for bids looks like this:

resources :auctions do
  resources :bids
end

We'd like to have a retract action that shows a form (and perhaps does some screening for retractability). The retract isn't the same as destroy; it's more like a portal to destroy. It's similar to edit, which serves as a form portal to update. Following the parallel with edit/update, we want a URL that looks like

/auctions/3/bids/5/retract

and a helper method called retract_auction_bid_url. The way you achieve this is by specifying an extra member route for the bids, as in Listing 3.1

Listing 3.1. Adding an extra member route

resources :auctions do
  resources :bids do
    member do
      get :retract
    end
  end
end

Then you can add a retraction link to your view using

link_to "Retract", retract_bid_path(auction, bid)

and the URL generated will include the /retract modifier. That said, you should probably let that link pull up a retraction form (and not trigger the retraction process itself!). The reason I say that is because, according to the tenets of HTTP, GET requests should not modify the state of the server; that's what POST requests are for.

So how do you trigger an actual retraction? Is it enough to add a :method option to link_to?

link_to "Retract", retract_bid_path(auction,bid), :method => :post

Not quite. Remember that in Listing 3.1 we defined the retract route as a get, so a POST will not be recognized by the routing system. The solution is to define an extra member route with post, like this:

resources :auctions do
  resources :bids do
    member do
      get :retract
      post :retract
    end
  end
end

If you're handling more than one HTTP verb with a single action, you should switch to using a single match declaration and a :via option, like this:

resources :auctions do
  resources :bids do
    member do
      match :retract, :via => [:get, :post]
    end
  end
end

Thanks to the flexibility of the routing system, we can tighten it up further using match with an :on option, like

resources :auctions do
  resources :bids do
    match :retract, :via => [:get, :post], :on => :member
  end
end

which would result in a route like this (output from rake routes):

retract_auction_bid GET|POST
/auctions/:auction_id/bids/:id/retract(.:format)
{:controller => "bids", :action => "retract"}

3.8.2 Extra Collection Routes

You can use the same routing technique to add routes that conceptually apply to an entire collection of resources:

resources :auctions do
  collection do
    match :terminate, :via => [:get, :post]
  end
end

In its shorter form:

resources :auctions do
  match :terminate, :via => [:get, :post], :on => :collection
end

This example will give you a terminate_auctions_path method, which will produce a URL mapping to the terminate action of the auctions controller. (A slightly bizarre example, perhaps, but the idea is that it would enable you to end all auctions at once.)

Thus you can fine-tune the routing behavior—even the RESTful routing behavior—of your application, so that you can arrange for special and specialized cases while still thinking in terms of resources.

3.8.3 Custom Action Names

Occasionally, you might want to deviate from the default naming convention for Rails RESTful routes. The :path_names option allows you to specify alternate name mappings. The example code shown changes the new and edit actions to Spanish-language equivalents.

resources :projects, :path_names => { :new => 'nuevo', :edit => 'cambiar'}

The URLs change (but the names of the generated helper methods do not).

new_report    GET     /reports/nuevo(.:format)
edit_report   GET     /reports/:id/cambiar(.:format)

3.8.4 Mapping to a Different Controller

You may use the :controller option to map a resource to a different controller than the one it would do so by default. This feature is occasionally useful for aliasing resources to a more natural controller name.

resources :photos, :controller => "images"

3.8.5 Routes for New Resources

The routing system has a neat syntax for specifying routes that only apply to new resources, ones that haven't been saved yet. You declare extra routes inside of a nested new block, like this:

resources :reports do
  new do
    post :preview
  end
end

The declaration above would result in the following route being defined.

preview_new_report POST   /reports/new/preview(.:format)
{:action=>"preview", :controller=>"reports"}

Refer to your new route within a view form by altering the default :url.

= form_for(report, :url => preview_new_report_path) do |f|
  ...
  = f.submit "Preview"

3.8.6 Considerations for Extra Routes

Referring to extra member and collection actions, David has been quoted as saying, "If you're writing so many additional methods that the repetition is beginning to bug you, you should revisit your intentions. You're probably not being as RESTful as you could be."

The last sentence is key. Adding extra actions corrupts the elegance of your overall RESTful application design, because it leads you away from finding all of the resources lurking in your domain.

Keeping in mind that real applications are more complicated than code examples in a reference book, let's see what would happen if we had to model retractions strictly using resources. Rather than tacking a retract action onto the BidsController, we might feel compelled to introduce a retraction resource, associated with bids, and write a RetractionController to handle it.

resources :bids do
  resource :retraction
end

RetractionController could now be in charge of everything having to do with retraction activities, rather than having that functionality mixed into BidsController. And if you think about it, something as weighty as bid retraction would eventually accumulate quite a bit of logic. Some would call breaking it out into its own controller proper separation of concerns or even just good object-orientation.

  • + Share This
  • 🔖 Save To Your Account