diff options
author | David Heinemeier Hansson <david@loudthinking.com> | 2007-04-26 01:53:01 +0000 |
---|---|---|
committer | David Heinemeier Hansson <david@loudthinking.com> | 2007-04-26 01:53:01 +0000 |
commit | 9b8399fb7f97c0b7f7f80ffc8bc2e74565fec642 (patch) | |
tree | 6ab89861f7a54e3d518659a833dd19764f7c4bcd /activeresource | |
parent | ddd243a9c11b54fd13328a86a34ae997f63cb839 (diff) | |
download | rails-9b8399fb7f97c0b7f7f80ffc8bc2e74565fec642.tar.gz rails-9b8399fb7f97c0b7f7f80ffc8bc2e74565fec642.tar.bz2 rails-9b8399fb7f97c0b7f7f80ffc8bc2e74565fec642.zip |
Added support for calling custom methods #6979 [rwdaigle]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@6584 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activeresource')
-rw-r--r-- | activeresource/CHANGELOG | 5 | ||||
-rw-r--r-- | activeresource/lib/active_resource.rb | 2 | ||||
-rw-r--r-- | activeresource/lib/active_resource/base.rb | 1 | ||||
-rw-r--r-- | activeresource/lib/active_resource/custom_methods.rb | 104 | ||||
-rw-r--r-- | activeresource/lib/active_resource/http_mock.rb | 8 | ||||
-rw-r--r-- | activeresource/test/base/custom_methods_test.rb | 87 | ||||
-rw-r--r-- | activeresource/test/base_test.rb | 6 |
7 files changed, 212 insertions, 1 deletions
diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index 5da64b75f2..807f887a96 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -1,5 +1,10 @@ *SVN* +* Added support for calling custom methods #6979 [rwdaigle] + + Person.find(:managers) # => GET /people/managers.xml + Kase.find(1).post(:close) # => POST /kases/1/close.xml + * Remove explicit prefix_options parameter for ActiveResource::Base#initialize. [Rick] ActiveResource splits the prefix_options from it automatically. diff --git a/activeresource/lib/active_resource.rb b/activeresource/lib/active_resource.rb index a1af676086..83039606af 100644 --- a/activeresource/lib/active_resource.rb +++ b/activeresource/lib/active_resource.rb @@ -37,9 +37,11 @@ end require 'active_resource/base' require 'active_resource/struct' require 'active_resource/validations' +require 'active_resource/custom_methods' module ActiveResource Base.class_eval do include Validations + include CustomMethods end end
\ No newline at end of file diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index cd1f07e294..6fa301d740 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -122,6 +122,7 @@ module ActiveResource case scope when :all then find_every(options) when :first then find_every(options).first + when Symbol then get(scope, options) else find_single(scope, options) end end diff --git a/activeresource/lib/active_resource/custom_methods.rb b/activeresource/lib/active_resource/custom_methods.rb new file mode 100644 index 0000000000..de99305b93 --- /dev/null +++ b/activeresource/lib/active_resource/custom_methods.rb @@ -0,0 +1,104 @@ +# Support custom methods and sub-resources for REST. +# +# Say you on the server configure your routes with: +# +# 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.xml #=> PeopleController.register +# PUT /people/1/promote.xml #=> PeopleController.promote with :id => 1 +# DELETE /people/1/deactivate.xml #=> PeopleController.deactivate with :id => 1 +# GET /people/active.xml #=> PeopleController.active +# +# This module provides the ability for Active Resource to call these +# custom REST methods and get the response back. +# +# class Person < ActiveResource::Base +# self.site = "http://37s.sunrise.i:3000" +# end +# +# Person.new(:name => 'Ryan).post(:register) #=> { :id => 1, :name => 'Ryan' } +# +# Person.find(1).put(:promote, :position => 'Manager') +# Person.find(1).delete(:deactivate) +# +# Person.get(:active) #=> [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}] +module ActiveResource + module CustomMethods + def self.included(within) + within.class_eval do + class << self + include ActiveResource::CustomMethods::ClassMethods + + alias :orig_delete :delete + + def get(method_name, options = {}) + connection.get(custom_method_collection_url(method_name, options)) + end + + def post(method_name, options = {}, body = nil) + connection.post(custom_method_collection_url(method_name, options), body) + end + + def put(method_name, options = {}, body = nil) + connection.put(custom_method_collection_url(method_name, options), body) + end + + # Need to jump through some hoops to retain the original class 'delete' method + def delete(custom_method_name, options = {}) + if (custom_method_name.is_a?(Symbol)) + connection.delete(custom_method_collection_url(custom_method_name, options)) + else + orig_delete(custom_method_name, options) + end + end + end + + end + + within.send(:include, ActiveResource::CustomMethods::InstanceMethods) + end + + module ClassMethods + def custom_method_collection_url(method_name, options = {}) + prefix_options, query_options = split_options(options) + "#{prefix(prefix_options)}#{collection_name}/#{method_name}.xml#{query_string(query_options)}" + end + end + + module InstanceMethods + def get(method_name, options = {}) + connection.get(custom_method_element_url(method_name, options)) + end + + def post(method_name, options = {}, body = nil) + if new? + connection.post(custom_method_new_element_url(method_name, options), (body.nil? ? to_xml : body)) + else + connection.post(custom_method_element_url(method_name, options), body) + end + end + + def put(method_name, options = {}, body = nil) + connection.put(custom_method_element_url(method_name, options), body) + end + + def delete(method_name, options = {}) + connection.delete(custom_method_element_url(method_name, options)) + end + + + private + def custom_method_element_url(method_name, options = {}) + "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{id}/#{method_name}.xml#{self.class.send(:query_string, options)}" + end + + def custom_method_new_element_url(method_name, options = {}) + "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/new/#{method_name}.xml#{self.class.send(:query_string, options)}" + end + end + end +end
\ No newline at end of file diff --git a/activeresource/lib/active_resource/http_mock.rb b/activeresource/lib/active_resource/http_mock.rb index 90823babd1..16ac62bf13 100644 --- a/activeresource/lib/active_resource/http_mock.rb +++ b/activeresource/lib/active_resource/http_mock.rb @@ -110,6 +110,14 @@ module ActiveResource def []=(key, value) headers[key] = value end + + def ==(other) + if (other.is_a?(Response)) + other.body == body && other.message == message && other.headers == headers + else + false + end + end end class Connection diff --git a/activeresource/test/base/custom_methods_test.rb b/activeresource/test/base/custom_methods_test.rb new file mode 100644 index 0000000000..b937fe8f9e --- /dev/null +++ b/activeresource/test/base/custom_methods_test.rb @@ -0,0 +1,87 @@ +require "#{File.dirname(__FILE__)}/../abstract_unit" +require "#{File.dirname(__FILE__)}/../fixtures/person" +require "#{File.dirname(__FILE__)}/../fixtures/street_address" + +class CustomMethodsTest < Test::Unit::TestCase + def setup + @matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person') + @matz_deep = { :id => 1, :name => 'Matz', :other => 'other' }.to_xml(:root => 'person') + @ryan = { :name => 'Ryan' }.to_xml(:root => 'person') + @addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address') + @addy_deep = { :id => 1, :street => '12345 Street', :zip => "27519" }.to_xml(:root => 'address') + @default_request_headers = { 'Content-Type' => 'application/xml' } + + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/people/1.xml", {}, @matz + mock.get "/people/1/shallow.xml", {}, @matz + mock.get "/people/1/deep.xml", {}, @matz_deep + mock.get "/people/retrieve.xml?name=Matz", {}, "<people>#{@matz}</people>" + mock.get "/people/managers.xml", {}, "<people>#{@matz}</people>" + mock.put "/people/1/promote.xml?position=Manager", {}, nil, 204 + mock.put "/people/promote.xml?name=Matz", {}, nil, 204, {} + mock.put "/people/sort.xml?by=name", {}, nil, 204 + mock.delete "/people/deactivate.xml?name=Matz", {}, nil, 200 + mock.delete "/people/1/deactivate.xml", {}, nil, 200 + mock.post "/people/new/register.xml", {}, @ryan, 201, 'Location' => '/people/5.xml' + mock.get "/people/1/addresses/1.xml", {}, @addy + mock.get "/people/1/addresses/1/deep.xml", {}, @addy_deep + mock.put "/people/1/addresses/1/normalize_phone.xml?locale=US", {}, nil, 204 + mock.put "/people/1/addresses/sort.xml?by=name", {}, nil, 204 + mock.post "/people/1/addresses/new/link.xml", {}, { :street => '12345 Street' }.to_xml(:root => 'address'), 201, 'Location' => '/people/1/addresses/2.xml' + end + end + + def teardown + ActiveResource::HttpMock.reset! + end + + def test_custom_collection_method + # GET + assert_equal([{ "id" => 1, "name" => 'Matz' }], Person.get(:retrieve, :name => 'Matz')) + + # PUT + assert_equal ActiveResource::Response.new("", 204, {}), + Person.put(:promote, {:name => 'Matz'}, 'atestbody') + assert_equal ActiveResource::Response.new("", 204, {}), Person.put(:sort, :by => 'name') + + # DELETE + Person.delete :deactivate, :name => 'Matz' + + # Nested resource + assert_equal ActiveResource::Response.new("", 204, {}), StreetAddress.put(:sort, :person_id => 1, :by => 'name') + end + + def test_custom_element_method + # Test GET against an element URL + assert_equal Person.find(1).get(:shallow), {"id" => 1, "name" => 'Matz'} + assert_equal Person.find(1).get(:deep), {"id" => 1, "name" => 'Matz', "other" => 'other'} + + # Test PUT against an element URL + assert_equal ActiveResource::Response.new("", 204, {}), Person.find(1).put(:promote, {:position => 'Manager'}, 'body') + + # Test DELETE against an element URL + assert_equal ActiveResource::Response.new("", 200, {}), Person.find(1).delete(:deactivate) + + # With nested resources + assert_equal StreetAddress.find(1, :person_id => 1).get(:deep), + { "id" => 1, "street" => '12345 Street', "zip" => "27519" } + assert_equal ActiveResource::Response.new("", 204, {}), + StreetAddress.find(1, :person_id => 1).put(:normalize_phone, :locale => 'US') + end + + def test_custom_new_element_method + # Test POST against a new element URL + ryan = Person.new(:name => 'Ryan') + assert_equal ActiveResource::Response.new(@ryan, 201, {'Location' => '/people/5.xml'}), ryan.post(:register) + + # Test POST against a nested collection URL + addy = StreetAddress.new(:street => '123 Test Dr.', :person_id => 1) + assert_equal ActiveResource::Response.new({ :street => '12345 Street' }.to_xml(:root => 'address'), + 201, {'Location' => '/people/1/addresses/2.xml'}), + addy.post(:link) + end + + def test_find_custom_resources + assert_equal [{ "id" => 1, "name" => 'Matz' }], Person.find(:managers) + end +end
\ No newline at end of file diff --git a/activeresource/test/base_test.rb b/activeresource/test/base_test.rb index 2c0a094b3a..374cb9481a 100644 --- a/activeresource/test/base_test.rb +++ b/activeresource/test/base_test.rb @@ -95,7 +95,11 @@ class BaseTest < Test::Unit::TestCase assert_equal '/people.xml?gender=', Person.collection_path(:gender => nil) assert_equal '/people.xml?gender=male', Person.collection_path('gender' => 'male') - assert_equal '/people.xml?gender=male&student=true', Person.collection_path(:gender => 'male', :student => true) + + # Use includes? because ordering of param hash is not guaranteed + assert Person.collection_path(:gender => 'male', :student => true).include?('/people.xml?') + assert Person.collection_path(:gender => 'male', :student => true).include?('gender=male') + assert Person.collection_path(:gender => 'male', :student => true).include?('student=true') assert_equal '/people.xml?name%5B%5D=bob&name%5B%5D=your+uncle%2Bme&name%5B%5D=&name%5B%5D=false', Person.collection_path(:name => ['bob', 'your uncle+me', nil, false]) |