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

Ticket #6979: active_resource_custom_methods_path.diff

File active_resource_custom_methods_path.diff, 13.0 kB (added by rwdaigle, 2 years ago)

Add client-side RESTful custom-method support to ActiveResource

  • test/base_test.rb

    old new  
    66class BaseTest < Test::Unit::TestCase 
    77  def setup 
    88    @matz  = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person') 
     9    @matz_deep  = { :id => 1, :name => 'Matz', :other => 'other' }.to_xml(:root => 'person') 
     10    @ryan  = { :name => 'Ryan' }.to_xml(:root => 'person') 
    911    @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person') 
    1012    @addy  = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address') 
     13    @addy_deep  = { :id => 1, :street => '12345 Street', :zip => "27519" }.to_xml(:root => 'address') 
    1114    @default_request_headers = { 'Content-Type' => 'application/xml' } 
    1215     
    1316    ActiveResource::HttpMock.respond_to do |mock| 
    1417      mock.get    "/people/1.xml",             {}, @matz 
     18      mock.get    "/people/1.xml;shallow", {}, @matz 
     19      mock.get    "/people/1.xml;deep", {}, @matz_deep 
     20      mock.get    "/people.xml;retrieve?name=Matz", {}, "<people>#{@matz}</people>" 
    1521      mock.get    "/people/2.xml",             {}, @david 
    1622      mock.put    "/people/1.xml",             {}, nil, 204 
     23      mock.put    "/people/1.xml;promote?position=Manager", {}, nil, 204 
     24      mock.put    "/people.xml;promote?name=Matz", {}, nil, 204 
     25      mock.put    "/people.xml;sort?by=name", {}, nil, 204 
    1726      mock.delete "/people/1.xml",             {}, nil, 200 
    1827      mock.delete "/people/2.xml",             {}, nil, 400 
     28      mock.delete "/people.xml;deactivate?name=Matz", {}, nil, 200 
     29      mock.delete "/people/1.xml;deactivate", {}, nil, 200 
    1930      mock.post   "/people.xml",               {}, nil, 201, 'Location' => '/people/5.xml' 
     31      mock.post   "/people.xml/new;register",      {}, @ryan, 201, 'Location' => '/people/5.xml' 
    2032      mock.get    "/people/99.xml",            {}, nil, 404 
    2133      mock.get    "/people.xml",               {}, "<people>#{@matz}#{@david}</people>" 
    2234      mock.get    "/people/1/addresses.xml",   {}, "<addresses>#{@addy}</addresses>" 
    2335      mock.get    "/people/1/addresses/1.xml", {}, @addy 
     36      mock.get    "/people/1/addresses/1.xml;deep", {}, @addy_deep 
    2437      mock.put    "/people/1/addresses/1.xml", {}, nil, 204 
     38      mock.put    "/people/1/addresses/1.xml;normalize_phone?locale=US", {}, nil, 204 
     39      mock.put    "/people/1/addresses.xml;sort?by=name", {}, nil, 204 
    2540      mock.delete "/people/1/addresses/1.xml", {}, nil, 200 
    2641      mock.post   "/people/1/addresses.xml",   {}, nil, 201, 'Location' => '/people/1/addresses/5' 
     42      mock.post   "/people/1/addresses.xml/new;link", {}, { :street => '12345 Street' }.to_xml(:root => 'address'), 201, 'Location' => '/people/1/addresses/2.xml' 
    2743      mock.get    "/people//addresses.xml",    {}, nil, 404 
    2844      mock.get    "/people//addresses/1.xml",  {}, nil, 404 
    2945      mock.put    "/people//addresses/1.xml",  {}, nil, 404 
     
    237253  def test_delete 
    238254    assert Person.delete(1) 
    239255  end 
     256   
     257  def test_custom_collection_method 
     258     
     259    Person.collection_call do |call| 
     260      call.get :retrieve 
     261      call.put :promote, :sort 
     262      call.delete :deactivate 
     263    end 
     264     
     265    # Test GET against a collection URL 
     266    assert_equal [Person.new(:id => 1, :name => 'Matz')], Person.retrieve(:name => 'Matz') 
     267     
     268    # Test PUT against a collection URL 
     269    assert Person.promote(:name => 'Matz') 
     270    assert Person.sort(:by => 'name') 
     271     
     272    # Test DELETE against a collection URL 
     273    assert Person.deactivate(:name => 'Matz') 
     274     
     275    # With nested resources 
     276    StreetAddress.collection_call do |call| 
     277      call.put :sort 
     278    end 
     279     
     280    # Test PUT against a nested collection URL 
     281    assert StreetAddress.sort(:person_id => 1, :by => 'name') 
     282  end 
     283   
     284  def test_custom_element_method 
     285     
     286    Person.element_call do |call| 
     287      call.get :shallow, :deep 
     288      call.put :promote 
     289      call.delete :deactivate 
     290    end 
     291     
     292    # Test GET against an element URL 
     293    assert_equal Person.new(:id => 1, :name => 'Matz'), Person.find(1).shallow 
     294    assert_equal Person.new(:id => 1, :name => 'Matz', :other => 'other'), Person.find(1).deep 
     295     
     296    # Test PUT against an element URL 
     297    assert Person.find(1).promote(:position => 'Manager') 
     298     
     299    # Test DELETE against an element URL 
     300    assert Person.find(1).deactivate 
     301     
     302    # With nested resources 
     303    StreetAddress.element_call do |call| 
     304      call.get :deep 
     305      call.put :normalize_phone 
     306    end 
     307     
     308    # Test GET against a nested collection URL 
     309    addy = StreetAddress.find(1, :person_id => 1) 
     310    assert !addy.defined?(:zip) 
     311    deep_addy = addy.deep 
     312    assert_not_nil deep_addy 
     313    assert_equal "27519", deep_addy.zip 
     314     
     315    # Test PUT against a nested collection URL 
     316    assert addy.normalize_phone(:locale => 'US') 
     317  end 
     318   
     319  def test_custom_new_element_method 
     320     
     321    Person.new_element_call do |call| 
     322      call.post :register 
     323    end 
     324     
     325    # Test POST against a new element URL 
     326    ryan = Person.new(:name => 'Ryan') 
     327    assert ryan.register 
     328    assert_equal '5', ryan.id 
     329     
     330    # With nested resources 
     331    StreetAddress.new_element_call do |call| 
     332      call.post :link 
     333    end 
     334     
     335    # Test POST against a nested collection URL 
     336    addy = StreetAddress.new({:street => '123 Test Dr.'}, {:person_id => 1}) 
     337    assert addy.link 
     338    assert_equal "2", addy.id 
     339  end 
    240340end 
  • lib/active_resource/custom_methods.rb

    old new  
     1# Support custom REST methods, in synch with the Simply RESTful 
     2# plugin. 
     3# 
     4# I.e. on the server routes config you would have: 
     5# 
     6#   map.resources :people, :new => { :register => :post }, 
     7#                          :element => { :promote => :put, :deactivate => :delete } 
     8#                          :collection => { :active => :get } 
     9# 
     10#  Which creates routes for the following http requests 
     11# 
     12#  POST: /people/new;register #=> PeopleController.register 
     13#  PUT: /people/1;promote #=> PeopleController.promote(:id => 1) 
     14#  DELETE: /people/1;deactivate #=> PeopleController.deactivate(:id => 1) 
     15#  GET: /people;active #=> PeopleController.active 
     16# 
     17# This module provides the ability for ActiveResource to call these 
     18# custom REST methods: 
     19# 
     20#   class Person < ActiveResource::Base 
     21#     self.site = "http://37s.sunrise.i:3000" 
     22# 
     23#     self.new_element_call do |call| 
     24#       call.post :register 
     25#     end 
     26# 
     27#     self.element_call do |call| 
     28#       call.put :promote 
     29#       call.delete :deactivate 
     30#     end 
     31# 
     32#     self.collection_call do |call| 
     33#       call.get :active 
     34#     end 
     35#   end 
     36# 
     37#   # Defined methods are now available as class/instance methods 
     38# 
     39#   ryan = Person.new(:name => 'Ryan) 
     40#   ryan.register  #=> true 
     41#   ryan.id #=> 1 
     42# 
     43#   ryan = Person.find(1) 
     44#   ryan.promote(:position => 'Manager') #=> true 
     45#   ryan.deactivate #=> true 
     46# 
     47#   Person.active #=> [<Person::xxx>, <Person::xxx>] 
     48#    
     49module ActiveResource 
     50  module CustomMethods 
     51       
     52    def self.included(within) 
     53      within.extend(ClassMethods) 
     54    end 
     55     
     56    module ClassMethods 
     57      def collection_call 
     58        yield ActiveResource::CollectionCall.new(self) 
     59      end 
     60      def element_call 
     61        yield ActiveResource::ElementCall.new(self) 
     62      end 
     63      def new_element_call 
     64        yield ActiveResource::NewElementCall.new(self) 
     65      end 
     66    end 
     67  end 
     68   
     69  # These feel bloated - revisit for more DRY... 
     70   
     71  class CollectionCall 
     72    def initialize(klass) 
     73      @klass = klass 
     74    end 
     75    def get(*method_names) 
     76      method_names.each do |method| 
     77        @klass.class_eval <<-end_eval 
     78          def #{@klass}.#{method}(url_options = {}) 
     79            collection = connection.get("\#{prefix(url_options)}\#{collection_name}.xml;#{method}\#{query_string(url_options)}") || [] 
     80            collection.collect! { |element| new(element, url_options) }             
     81          end 
     82        end_eval 
     83      end 
     84    end 
     85    def put(*method_names) 
     86      method_names.each do |method| 
     87        @klass.class_eval <<-end_eval 
     88          def #{@klass}.#{method}(url_options = {}) 
     89            connection.put("\#{prefix(url_options)}\#{collection_name}.xml;#{method}\#{query_string(url_options)}") 
     90          end 
     91        end_eval 
     92      end 
     93    end 
     94    def delete(*method_names) 
     95      method_names.each do |method| 
     96        @klass.class_eval <<-end_eval 
     97          def #{@klass}.#{method}(url_options = {}) 
     98            connection.delete("\#{prefix(url_options)}\#{collection_name}.xml;#{method}\#{query_string(url_options)}") 
     99          end 
     100        end_eval 
     101      end 
     102    end 
     103  end 
     104   
     105  class ElementCall 
     106    def initialize(klass) 
     107      @klass = klass 
     108    end 
     109    def get(*method_names) 
     110      method_names.each do |method| 
     111        @klass.class_eval <<-end_eval 
     112          def #{method}(url_options = {}) 
     113            #{@klass}.new(connection.get("\#{self.class.prefix(prefix_options)}\#{self.class.collection_name}/\#{id}.xml;#{method}\#{self.class.query_string(url_options)}")) 
     114          end 
     115        end_eval 
     116      end 
     117    end 
     118    def put(*method_names) 
     119      method_names.each do |method| 
     120        @klass.class_eval <<-end_eval 
     121          def #{method}(url_options = {}) 
     122            connection.put("\#{self.class.prefix(prefix_options)}\#{self.class.collection_name}/\#{id}.xml;#{method}\#{self.class.query_string(url_options)}") 
     123          end 
     124        end_eval 
     125      end 
     126    end 
     127    def delete(*method_names) 
     128      method_names.each do |method| 
     129        @klass.class_eval <<-end_eval 
     130          def #{method}(url_options = {}) 
     131            connection.delete("\#{self.class.prefix(prefix_options)}\#{self.class.collection_name}/\#{id}.xml;#{method}\#{self.class.query_string(url_options)}") 
     132          end 
     133        end_eval 
     134      end 
     135    end 
     136  end 
     137   
     138  class NewElementCall 
     139    def initialize(klass) 
     140      @klass = klass 
     141    end 
     142    def post(*method_names) 
     143      method_names.each do |method| 
     144        @klass.class_eval <<-end_eval 
     145          def #{method}(url_options = {}) 
     146            returning(connection.post("\#{self.class.prefix(prefix_options)}\#{self.class.collection_name}.xml/new;#{method}", to_xml)) do |response| 
     147              self.id = id_from_response(response) 
     148            end 
     149          end 
     150        end_eval 
     151      end 
     152    end 
     153  end 
     154end 
  • lib/active_resource/base.rb

    old new  
    8080        connection.delete(element_path(id)) 
    8181      end 
    8282 
     83      # This was private but was needed in CustomMethods, pretty sure 
     84      # there's a better way to access it other than to promote it to 
     85      # public. 
     86      def query_string(options) 
     87        # Omit parameters which appear in the URI path. 
     88        query_params = options.reject { |key, value| prefix_parameters.include?(key) } 
     89 
     90        # Accumulate a list of escaped key=value pairs for the given parameters. 
     91        pairs = [] 
     92        query_params.each do |key, value| 
     93          key = CGI.escape(key.to_s) 
     94 
     95          # a => b becomes a=b 
     96          # a => [b, c] becomes a[]=b&a[]=c 
     97          case value 
     98            when Array 
     99              value.each { |val| pairs << "#{key}[]=#{CGI.escape(val.to_s)}" } 
     100            else 
     101              pairs << "#{key}=#{CGI.escape(value.to_s)}" 
     102          end 
     103        end 
     104 
     105        "?#{pairs * '&'}" unless pairs.empty? 
     106      end 
     107 
    83108      private 
    84109        def find_every(options) 
    85110          collection = connection.get(collection_path(options)) || [] 
     
    98123        def prefix_parameters 
    99124          @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set 
    100125        end 
    101  
    102         def query_string(options) 
    103           # Omit parameters which appear in the URI path. 
    104           query_params = options.reject { |key, value| prefix_parameters.include?(key) } 
    105  
    106           # Accumulate a list of escaped key=value pairs for the given parameters. 
    107           pairs = [] 
    108           query_params.each do |key, value| 
    109             key = CGI.escape(key.to_s) 
    110  
    111             # a => b becomes a=b 
    112             # a => [b, c] becomes a[]=b&a[]=c 
    113             case value 
    114               when Array 
    115                 value.each { |val| pairs << "#{key}[]=#{CGI.escape(val.to_s)}" } 
    116               else 
    117                 pairs << "#{key}=#{CGI.escape(value.to_s)}" 
    118             end 
    119           end 
    120  
    121           "?#{pairs * '&'}" unless pairs.empty? 
    122         end 
    123126    end 
    124127 
    125128    attr_accessor :attributes 
  • lib/active_resource.rb

    old new  
    3737require 'active_resource/base' 
    3838require 'active_resource/struct' 
    3939require 'active_resource/validations' 
     40require 'active_resource/custom_methods' 
    4041 
    4142module ActiveResource 
    4243  Base.class_eval do 
    4344    include Validations 
     45    include CustomMethods 
    4446  end 
    4547end