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

Ticket #6979 (closed enhancement: fixed)

Opened 2 years ago

Last modified 2 months ago

[PATCH] Add ability to specify custom method calls in ActiveResource

Reported by: rwdaigle Assigned to: technoweenie
Priority: normal Milestone: 1.x
Component: ActiveResource Version: edge
Severity: normal Keywords: custom methods, REST
Cc: bitsweat, technoweenie, Erkki

Description

ActiveResource does not currently support the invocation of custom method calls (which are available in the Simply RESTful routing). This patch lets custom methods be RESTfully invoked from ActiveResource:

Support custom REST methods, in synch with the Simply RESTful plugin.

I.e. on the server routes config you would have:

map.resources :people, :new => { :register => :post },
                         :element => { :promote => :put, :deactivate => :delete }
                         :collection => { :active => :get }

Which creates routes for the following http requests

 POST: /people/new;register #=> PeopleController.register
 PUT: /people/1;promote #=> PeopleController.promote(:id => 1)
 DELETE: /people/1;deactivate #=> PeopleController.deactivate(:id => 1)
 GET: /people;active #=> PeopleController.active

This module provides the ability for ActiveResource to call these custom REST methods:

  class Person < ActiveResource::Base
    self.site = "http://37s.sunrise.i:3000"

    self.new_element_call do |call|
      call.post :register
    end

    self.element_call do |call|
      call.put :promote
      call.delete :deactivate
    end

    self.collection_call do |call|
      call.get :active
    end
  end

  # Defined methods are now available as class/instance methods

  # Element calls are available as instance methods
  ryan = Person.new(:name => 'Ryan)
  ryan.register  #=> true
  ryan.id #=> 1

  ryan = Person.find(1)
  ryan.promote(:position => 'Manager') #=> true
  ryan.deactivate #=> true

  # Collection calls are available as class methods
  Person.active #=> [<Person::xxx>, <Person::xxx>]

Attachments

active_resource_custom_methods_path.diff (13.0 kB) - added by rwdaigle on 01/10/07 16:21:11.
Add client-side RESTful custom-method support to ActiveResource
active_resource_custom_methods_path_consolidated.diff (13.1 kB) - added by rwdaigle on 01/12/07 02:58:56.
custom methods using more concise definition syntax (single block)
active_resource_custom_methods_path_unblocked.diff (11.3 kB) - added by rwdaigle on 01/12/07 19:25:46.
Add client-side RESTful custom-method support to ActiveResource (using simpler non-blocked syntax)
active_resource_custom_methods_patch_unblocked_fixed_tests.diff (10.5 kB) - added by rwdaigle on 01/16/07 16:12:43.
Request syntax custom methods functionality with tests fixed.

Change History

01/10/07 16:21:11 changed by rwdaigle

  • attachment active_resource_custom_methods_path.diff added.

Add client-side RESTful custom-method support to ActiveResource

(follow-up: ↓ 3 ) 01/10/07 20:53:56 changed by technoweenie

  • owner changed from core to technoweenie.
  • status changed from new to assigned.

+++1, though I don't really care for the block format for specifying methods. Did you think about something like:

post :new, :register
put :member, :promote
delete :member, :deactivate
get :collection, :active

new :post, :register
member :put, :promote
member :delete, :deactivate
collection :get, :active

new :register # POST is assumed?
member :promote, :method => :put
member :deactivate, :method => :delete
# custom method name on the ARes class
collection :activate, :method => :get, :method => :find_activated

Just some thoughts. Though, #new would probably clash with ruby :)

(in reply to: ↑ description ) 01/10/07 22:56:33 changed by leonlleslie

Replying to rwdaigle:

ActiveResource does not currently support the invocation of custom method calls (which are available in the Simply RESTful routing). This patch lets custom methods be RESTfully invoked from ActiveResource: Support custom REST methods, in synch with the Simply RESTful plugin. I.e. on the server routes config you would have: {{{ map.resources :people, :new => { :register => :post }, :element => { :promote => :put, :deactivate => :delete } :collection => { :active => :get } }}} Which creates routes for the following http requests {{{ POST: /people/new;register #=> PeopleController.register PUT: /people/1;promote #=> PeopleController.promote(:id => 1) DELETE: /people/1;deactivate #=> PeopleController.deactivate(:id => 1) GET: /people;active #=> PeopleController.active }}} This module provides the ability for ActiveResource to call these custom REST methods: {{{ class Person < ActiveResource::Base self.site = "http://37s.sunrise.i:3000" self.new_element_call do |call| call.post :register end self.element_call do |call| call.put :promote call.delete :deactivate end self.collection_call do |call| call.get :active end end # Defined methods are now available as class/instance methods # Element calls are available as instance methods ryan = Person.new(:name => 'Ryan) ryan.register #=> true ryan.id #=> 1 ryan = Person.find(1) ryan.promote(:position => 'Manager') #=> true ryan.deactivate #=> true # Collection calls are available as class methods Person.active #=> [<Person::xxx>, <Person::xxx>] }}}

This looks like a cool feature for ActiveRecord you have my vote.

(in reply to: ↑ 1 ) 01/11/07 00:56:32 changed by rwdaigle

  • keywords changed from 'custom methods', REST to custom methods, REST.

Replying to technoweenie:

... I don't really care for the block format ...

I did actually think of a non-blocked format - in fact that was my original implementation. However, I didn't feel like it was very.. tight? Having those very generic get or put method definitions seemed too apt for conflict with other methods. Plus having them invoked inline felt very haphazard and a little too open.

I chose to use a block because it keeps the custom method definitions to their own little piece of real estate, very clearly defined from the other parts of the ARes class. respond_to was my inspiration - but perhaps it was misplaced inspiration.

In looking over my patch I think I would like to keep a block format - but maybe contain all method types (collection, element, new) within a single block. I.e.:

  class Person < ActiveResource::Base
    self.site = "http://37s.sunrise.i:3000"

    self.invokes_custom_method do |invokes|
    
      # POST /people/new;register
      invokes :register, :on => :new, :using => :post

      # PUT /people/1.xml;promote
      invokes :promote, :on => :element, :using => :put

      # DELETE /people/1.xml;deactivate
      invokes :deactivate, :on => :element, :using => :delete

      # GET /people.xml;active
      invokes :active, :on => :collection, :using => :get
    end
  end

Does this feel better? It definitely reads nicely... Let me know what you think - I'd like to get started on this if you think this is the right direction. And if I'm being difficult let me know that too :)

01/12/07 02:57:46 changed by rwdaigle

I updated the patch (see the _consolidated.patch) so that the syntax would like so:

class Person < ActiveResource::Base

  self.site = "http://37s.sunrise.i:3000"

  self.invokes_custom_method do |person|
    person.invokes :register, :on => :new, :using => :post
    person.invokes :promote, :using => :put
    person.invokes :deactivate, :using => :delete
    person.invokes :active, :on => :collection
  end
end

Does that feel better?

I could use some pointers on the body of custom_methods.rb though - the implementation feels very bloated and repetitive.

01/12/07 02:58:56 changed by rwdaigle

  • attachment active_resource_custom_methods_path_consolidated.diff added.

custom methods using more concise definition syntax (single block)

01/12/07 06:35:30 changed by bitsweat

  • cc set to bitsweat.

There is no room for logic in this style. In every case I've defined new Ares methods, I need to take the response and check the return code, parse the body, or something. So my vote's for an easier way to wrap

connection.post("#{element_path(id)};action")

with

response = post :element, :action

01/12/07 19:25:02 changed by rwdaigle

Ok - I hear you about the connection wrapping (though I still love the DSL-sweetness of my proposed format).

I've attached a new patch (see _unblocked.diff) that implements this simpler syntax and returns the response from the connection call.

# -- Posts on a new instance are executed against the 'new' REST url

# POST /people.xml/new;register
Person.new(:name => 'Ryan).post(:register) #=> {:id => 1, :name => 'Ryan'}

# -- Instance calls are executed against the element REST url

# PUT /people/1.xml;promote
Person.find(1).put(:promote, :position => 'Manager') #=> <Response::XXXX>

# DELETE /people/1.xml;deactivate
Person.find(1).delete(:deactivate) #=> <Response::XXXX>

# -- Class calls are executed against the collection URL

# GET /people.xml;active
Person.get(:active) #=> [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]

01/12/07 19:25:46 changed by rwdaigle

  • attachment active_resource_custom_methods_path_unblocked.diff added.

Add client-side RESTful custom-method support to ActiveResource (using simpler non-blocked syntax)

(follow-up: ↓ 8 ) 01/16/07 06:41:41 changed by technoweenie

  • cc changed from bitsweat to bitsweat, technoweenie.

I'm getting a lot of errors like this:

test_update_with_custom_prefix(BaseTest):
ActiveResource::InvalidRequestError: No response recorded for: #<ActiveResource::Request:0x14f7510 @method=:put, @path="/people/1/addresses/1.xml", @headers={"Content-Type"=>"application/xml"}, @body="<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<address>\n  <id type=\"integer\">1</id>\n  <street>54321 Street</street>\n</address>\n">
    (eval):4:in `put'
    ./test/../lib/active_resource/connection.rb:81:in `request'
    /opt/local/lib/ruby/1.8/benchmark.rb:300:in `measure'
    /opt/local/lib/ruby/1.8/benchmark.rb:314:in `realtime'
    ./test/../lib/active_resource/connection.rb:81:in `request'
    ./test/../lib/active_resource/connection.rb:67:in `put'
    ./test/../lib/active_resource/base.rb:238:in `update'
    ./test/../lib/active_resource/base.rb:186:in `save_without_validation'
    ./test/../lib/active_resource/validations.rb:145:in `save'
    ./test/base_test.rb:212:in `test_update_with_custom_prefix'

Did you forget any files?

(in reply to: ↑ 7 ) 01/16/07 16:11:33 changed by rwdaigle

Replying to technoweenie:

I'm getting a lot of errors...

I didn't forget any files - but it looks as though I didn't name the CustomMethodsTest class correctly (leftover from a copy from BaseTest) - so there are weird conflicts. Attaching new patch with fixed tests (including a test in BaseTest that was failing for me). My apologies.

01/16/07 16:12:43 changed by rwdaigle

  • attachment active_resource_custom_methods_patch_unblocked_fixed_tests.diff added.

Request syntax custom methods functionality with tests fixed.

04/25/07 12:03:45 changed by Erkki

  • cc changed from bitsweat, technoweenie to bitsweat, technoweenie, Erkki.

04/26/07 01:54:20 changed by david

  • status changed from assigned to closed.
  • resolution set to fixed.

Closed by [6584].