Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #11022 (new enhancement)

Opened 1 year ago

Last modified 1 year ago

[PATCH] HTTP PUT to a resource collection should be mapped to a standard controller action.

Reported by: cch1 Assigned to: core
Priority: normal Milestone: 2.x
Component: ActionPack Version: edge
Severity: normal Keywords: PUT collection REST resources
Cc: trevor@protocool.com

Description

Rails supports mapping of REST verbs to the controller actions that were historically common in Rails apps when REST support was integrated. In the case of resource collections, these mappings are:

  • GET => index
  • POST => create

Many collections support "replace all elements with X" semantics. Currently, it is necessary to add a meaningless suffix to any collection URI that needs to support such an operation:

map.resources :tags, :collection => {:blah => :put}

which requires an ugly URL like http://mysite.com/tags/blah.

Similarly, some collections support "delete all elements" semantics and again, an ugly URL is currently required.

This patch adds support for mapping PUT and DELETE against resource collections:

  • PUT => replace
  • DELETE => obliterate

Now the required URL is purty: http://mysite.com/tags

The replace action would typically expect an array of entities that would completely replace the collection specified in the URI. This interpretation of the PUT operation against a collection of resources is generally accepted and consistent with Rails' existing treatment of POST against a collection and PUT against a member.

The obliterate action would typically destroy all the elements in the collection specified by the URI. Again, this interpretation of DELETE against a collection is generally accepted and consistent with existing Rails' behavior.

References:

  1. http://www.artima.com/lejava/articles/why_put_and_delete.html
  2. http://rest.blueoxen.net/cgi-bin/wiki.pl?MinimumMethods#nid5YR

Attachments

11022.diff (5.2 kB) - added by cch1 on 02/08/08 23:51:44.
Patch with actual patch and tests.

Change History

02/05/08 22:57:24 changed by cch1

I've attached the patch, which includes tests. Note that I had to disable the existing suspiciously-named test "test_should_not_allow_delete_or_put_on_collection_path"

02/06/08 02:00:27 changed by protocool

Sorry but I really disagree with DELETE and so I can't +1.

There is an apparent lack of symmetry here:

DELETE /people/1
GET /people/1 #=> 404

DELETE/people
GET /people #=> 404?  I think not

I feel that PUTting an empty set is the correct way to achieve "delete everything". As in:

  • If /people is an empty set and you PUT 10 subordinates into /people that makes sense.
  • If /people has 10 subordinates and you PUT an empty set, the existing set of 10 subordinates will be replaced with an empty set of subordinates - and it recognizes that /people as a resource always exists.

You already know I'm in favor of PUT - is there any chance you can put the DELETE enhancement in another ticket and scale this back to just PUT?

Trev

02/06/08 04:11:47 changed by cch1

protocool: I had an uneasy feeling about DELETE at one point, but I've found good reason to accept it (more on that in a moment).

First, on the subject of symmetry: REST doesn't guarantee that all resources will behave the same to the same state transfer. There is usually an understanding that a URI that accepts entities for processing (collection) is different from a URI that represents a single entity. There are other examples (in practice) that exhibit a distinct lack of symmetry:

POST /people/1  => normally an error
POST /people    => ok, append the entity to the collection

Second, there are some design patterns that will need a DELETE on a collection. IIRC, the composite pattern strives for a common set of operations on leaves and interior nodes. If we block DELETE on collections (the logical representation of interior nodes), we've denied REST to that pattern.

Lastly, "in theory" there is nothing preventing my application from returning a 404 after a DELETE on a collection. And, while strange, there are valid cases for a POST to a member entity (appending content to a log, for example).

My point is this: PUT against a collection, DELETE against a collection, or POST against a member are all likely to be valid semantics for somebody. I'm in favor of providing the avenue for them to use RoR REST if that's their cup of tea.

All that having been said, I'm willing to split the ticket if that's what it takes to get your +1 -I appreciate your early support on this. Just say the word.

02/08/08 16:27:12 changed by cch1

  • component changed from ActiveRecord to ActionPack.

02/08/08 16:51:21 changed by cch1

See #11040 for the ActiveRecord corollary to this ticket.

(follow-ups: ↓ 7 ↓ 8 ) 02/08/08 19:48:39 changed by bitsweat

:collection => { :index => :any } doesn't work for you?

(in reply to: ↑ 6 ) 02/08/08 21:57:06 changed by cch1

Replying to bitsweat:

:collection => { :index => :any } doesn't work for you?

Wouldn't that simply route PUT on the collection to index? It should route to a specific action for replacing the contents of the collection. Likewise, DELETE should route to a specific action for deleting the collection.

-Chris

02/08/08 23:51:44 changed by cch1

  • attachment 11022.diff added.

Patch with actual patch and tests.

(in reply to: ↑ 6 ) 02/09/08 00:20:25 changed by protocool

Replying to bitsweat:

:collection => { :index => :any } doesn't work for you?

I agree with Chris. Routing all of the verbs to a single method gives you:

def index
  if request.put?
  elsif request.delete?
  elsif request.get?
  end
end

which is a pretty fat method even without respond_to blocks.

Routing PUT and DELETE to replace() and clear() nicely rounds out resources and maps very well to the concept of a 'collection' and to the already-familiar [].clear() and [].replace() methods.

I've tested the patch and, pending documentation patches explaining the routes and methods, consider it a hearty +1.

(follow-up: ↓ 10 ) 02/09/08 00:33:02 changed by bitsweat

You said

"Currently, it is necessary to add a meaningless suffix to any collection URI that needs to support such an operation:"

I suggested that is not the case.

Anyway. The patch looks good, though it does add two extremely-rarely-used routes per resource to every app out there.

(in reply to: ↑ 9 ; follow-up: ↓ 12 ) 02/11/08 17:25:23 changed by cch1

Replying to bitsweat:

You said "Currently, it is necessary to add a meaningless suffix to any collection URI that needs to support such an operation:" I suggested that is not the case. Anyway. The patch looks good, though it does add two extremely-rarely-used routes per resource to every app out there.

I don't suppose that is a +1, is it?

02/11/08 18:50:23 changed by kampers

  • summary changed from HTTP PUT to a resource collection should be mapped to a standard controller action. to [PATCH] HTTP PUT to a resource collection should be mapped to a standard controller action..

So, would you want to add these actions to the resource generators as well?

(in reply to: ↑ 10 ) 02/11/08 20:21:44 changed by bitsweat

Replying to cch1:

I don't suppose that is a +1, is it?

I'd call it a -0.1 ;)

02/19/08 01:30:43 changed by therealadam

I'm inclined to side with bitsweat on this one. Adding some rarely used routes to all apps doesn't sound core-ish. I think this would make a great plugin, though.

02/19/08 23:28:38 changed by cch1

After benchmarking (thanks technoweenie/lifo/nzkoz) the impact of this patch, I bow to the inevitable: current route recognition code in ActionController::Routing::Routes.recognize_path charges a high price for each new route and this patch adds two per resource.

Unfortunately, this is probably a poor candidate for a plugin for the same reason (performance) and because the code change in resources.rb is small (one or two lines) compared with the existing code in the relevant methods.

For those who want this external API badly enough, you can roll your own with something like this:

map.connect '/places', :controller => 'places', :action => 'replace',
 :conditions => {:method => :put}