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

Ticket #6979: active_resource_custom_methods_path_consolidated.diff

File active_resource_custom_methods_path_consolidated.diff, 13.1 kB (added by rwdaigle, 2 years ago)

custom methods using more concise definition syntax (single block)

  • test/base/custom_methods_test.rb

    old new  
     1require "#{File.dirname(__FILE__)}/../abstract_unit" 
     2require "#{File.dirname(__FILE__)}/../fixtures/person" 
     3require "#{File.dirname(__FILE__)}/../fixtures/street_address" 
     4 
     5class BaseTest < Test::Unit::TestCase 
     6  def setup 
     7    @matz  = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person') 
     8    @matz_deep  = { :id => 1, :name => 'Matz', :other => 'other' }.to_xml(:root => 'person') 
     9    @ryan  = { :name => 'Ryan' }.to_xml(:root => 'person') 
     10    @addy  = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address') 
     11    @addy_deep  = { :id => 1, :street => '12345 Street', :zip => "27519" }.to_xml(:root => 'address') 
     12    @default_request_headers = { 'Content-Type' => 'application/xml' } 
     13     
     14    ActiveResource::HttpMock.respond_to do |mock| 
     15      mock.get    "/people/1.xml",             {}, @matz 
     16      mock.get    "/people/1.xml;shallow", {}, @matz 
     17      mock.get    "/people/1.xml;deep", {}, @matz_deep 
     18      mock.get    "/people.xml;retrieve?name=Matz", {}, "<people>#{@matz}</people>" 
     19      mock.put    "/people/1.xml;promote?position=Manager", {}, nil, 204 
     20      mock.put    "/people.xml;promote?name=Matz", {}, nil, 204 
     21      mock.put    "/people.xml;sort?by=name", {}, nil, 204 
     22      mock.delete "/people.xml;deactivate?name=Matz", {}, nil, 200 
     23      mock.delete "/people/1.xml;deactivate", {}, nil, 200 
     24      mock.post   "/people.xml/new;register",      {}, @ryan, 201, 'Location' => '/people/5.xml' 
     25      mock.get    "/people/1/addresses/1.xml", {}, @addy 
     26      mock.get    "/people/1/addresses/1.xml;deep", {}, @addy_deep 
     27      mock.put    "/people/1/addresses/1.xml;normalize_phone?locale=US", {}, nil, 204 
     28      mock.put    "/people/1/addresses.xml;sort?by=name", {}, nil, 204 
     29      mock.post   "/people/1/addresses.xml/new;link", {}, { :street => '12345 Street' }.to_xml(:root => 'address'), 201, 'Location' => '/people/1/addresses/2.xml' 
     30    end 
     31  end 
     32   
     33  def test_custom_collection_method 
     34     
     35    Person.invokes_custom_method do |person| 
     36      person.invokes :retrieve, :on => :collection, :using => :get 
     37      person.invokes [:promote, :sort], :on => :collection, :using => :put 
     38      person.invokes :deactivate, :on => :collection, :using => :delete 
     39    end 
     40     
     41    # Test GET against a collection URL 
     42    assert_equal [Person.new(:id => 1, :name => 'Matz')], Person.retrieve(:name => 'Matz') 
     43     
     44    # Test PUT against a collection URL 
     45    assert Person.promote(:name => 'Matz') 
     46    assert Person.sort(:by => 'name') 
     47     
     48    # Test DELETE against a collection URL 
     49    assert Person.deactivate(:name => 'Matz') 
     50     
     51    # With nested resources 
     52    StreetAddress.invokes_custom_method do |address| 
     53      address.invokes :sort, :on => :collection, :using => :put 
     54    end 
     55     
     56    # Test PUT against a nested collection URL 
     57    assert StreetAddress.sort(:person_id => 1, :by => 'name') 
     58  end 
     59   
     60  def test_custom_element_method 
     61     
     62    Person.invokes_custom_method do |person| 
     63      person.invokes [:shallow, :deep] 
     64      person.invokes :promote, :using => :put 
     65      person.invokes :deactivate, :using => :delete 
     66    end 
     67     
     68    # Test GET against an element URL 
     69    assert_equal Person.new(:id => 1, :name => 'Matz'), Person.find(1).shallow 
     70    assert_equal Person.new(:id => 1, :name => 'Matz', :other => 'other'), Person.find(1).deep 
     71     
     72    # Test PUT against an element URL 
     73    assert Person.find(1).promote(:position => 'Manager') 
     74     
     75    # Test DELETE against an element URL 
     76    assert Person.find(1).deactivate 
     77     
     78    # With nested resources 
     79    StreetAddress.invokes_custom_method do |address| 
     80      address.invokes :deep 
     81      address.invokes :normalize_phone, :using => :put 
     82    end 
     83     
     84    # Test GET against a nested collection URL 
     85    addy = StreetAddress.find(1, :person_id => 1) 
     86    assert !addy.defined?(:zip) 
     87    deep_addy = addy.deep 
     88    assert_not_nil deep_addy 
     89    assert_equal "27519", deep_addy.zip 
     90     
     91    # Test PUT against a nested collection URL 
     92    assert addy.normalize_phone(:locale => 'US') 
     93  end 
     94   
     95  def test_custom_new_element_method 
     96     
     97    Person.invokes_custom_method do |person| 
     98      person.invokes :register, :on => :new, :using => :post 
     99    end 
     100     
     101    # Test POST against a new element URL 
     102    ryan = Person.new(:name => 'Ryan') 
     103    assert ryan.register 
     104    assert_equal '5', ryan.id 
     105     
     106    # With nested resources 
     107    StreetAddress.invokes_custom_method do |address| 
     108      address.invokes :link, :on => :new, :using => :post 
     109    end 
     110     
     111    # Test POST against a nested collection URL 
     112    addy = StreetAddress.new({:street => '123 Test Dr.'}, {:person_id => 1}) 
     113    assert addy.link 
     114    assert_equal "2", addy.id 
     115  end 
     116end 
  • 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.invokes_custom_method do |person| 
     24#       person.invokes :register, :on => :new, :using => :post 
     25#       person.invokes :promote, :using => :put 
     26#       person.invokes :deactivate, :using => :delete 
     27#       person.invokes :active, :on => :collection 
     28#     end 
     29#   end 
     30# 
     31#   # Defined methods are now available as class/instance methods 
     32# 
     33#   ryan = Person.new(:name => 'Ryan) 
     34#   ryan.register  #=> true 
     35#   ryan.id #=> 1 
     36# 
     37#   ryan = Person.find(1) 
     38#   ryan.promote(:position => 'Manager') #=> true 
     39#   ryan.deactivate #=> true 
     40# 
     41#   Person.active #=> [<Person::xxx>, <Person::xxx>] 
     42#    
     43module ActiveResource 
     44  module CustomMethods 
     45       
     46    def self.included(within) 
     47      within.extend(ClassMethods) 
     48    end 
     49     
     50    module ClassMethods 
     51      def invokes_custom_method 
     52        yield ActiveResource::CustomMethodProxy.new(self) 
     53      end 
     54    end 
     55  end 
     56   
     57  class CustomMethodProxy 
     58     
     59    DEFAULT_OPTIONS = {:on => :element, :using => :get} 
     60     
     61    def initialize(klass) 
     62      @klass = klass 
     63    end 
     64     
     65    def invokes(custom_methods, options = {}) 
     66       
     67      method = options[:using] || DEFAULT_OPTIONS[:using] 
     68      type = options[:on] || DEFAULT_OPTIONS[:on] 
     69       
     70      case type 
     71      when :collection then 
     72        CollectionCall.new(@klass).send(method, custom_methods) 
     73      when :element then 
     74        ElementCall.new(@klass).send(method, custom_methods) 
     75      when :new then 
     76        NewElementCall.new(@klass).send(method, custom_methods) 
     77      end 
     78    end 
     79  end 
     80   
     81  # These feel bloated - revisit for more DRY... 
     82   
     83  class CollectionCall 
     84    def initialize(klass) 
     85      @klass = klass 
     86    end 
     87    def get(*method_names) 
     88      method_names.flatten.each do |method| 
     89        @klass.class_eval <<-end_eval 
     90          def #{@klass}.#{method}(url_options = {}) 
     91            collection = connection.get("\#{prefix(url_options)}\#{collection_name}.xml;#{method}\#{query_string(url_options)}") || [] 
     92            collection.collect! { |element| new(element, url_options) }             
     93          end 
     94        end_eval 
     95      end 
     96    end 
     97    def put(*method_names) 
     98      method_names.flatten.each do |method| 
     99        @klass.class_eval <<-end_eval 
     100          def #{@klass}.#{method}(url_options = {}) 
     101            connection.put("\#{prefix(url_options)}\#{collection_name}.xml;#{method}\#{query_string(url_options)}") 
     102          end 
     103        end_eval 
     104      end 
     105    end 
     106    def delete(*method_names) 
     107      method_names.flatten.each do |method| 
     108        @klass.class_eval <<-end_eval 
     109          def #{@klass}.#{method}(url_options = {}) 
     110            connection.delete("\#{prefix(url_options)}\#{collection_name}.xml;#{method}\#{query_string(url_options)}") 
     111          end 
     112        end_eval 
     113      end 
     114    end 
     115  end 
     116   
     117  class ElementCall 
     118    def initialize(klass) 
     119      @klass = klass 
     120    end 
     121    def get(*method_names) 
     122      method_names.flatten.each do |method| 
     123        @klass.class_eval <<-end_eval 
     124          def #{method}(url_options = {}) 
     125            #{@klass}.new(connection.get("\#{self.class.prefix(prefix_options)}\#{self.class.collection_name}/\#{id}.xml;#{method}\#{self.class.query_string(url_options)}")) 
     126          end 
     127        end_eval 
     128      end 
     129    end 
     130    def put(*method_names) 
     131      method_names.flatten.each do |method| 
     132        @klass.class_eval <<-end_eval 
     133          def #{method}(url_options = {}) 
     134            connection.put("\#{self.class.prefix(prefix_options)}\#{self.class.collection_name}/\#{id}.xml;#{method}\#{self.class.query_string(url_options)}") 
     135          end 
     136        end_eval 
     137      end 
     138    end 
     139    def delete(*method_names) 
     140      method_names.flatten.each do |method| 
     141        @klass.class_eval <<-end_eval 
     142          def #{method}(url_options = {}) 
     143            connection.delete("\#{self.class.prefix(prefix_options)}\#{self.class.collection_name}/\#{id}.xml;#{method}\#{self.class.query_string(url_options)}") 
     144          end 
     145        end_eval 
     146      end 
     147    end 
     148  end 
     149   
     150  class NewElementCall 
     151    def initialize(klass) 
     152      @klass = klass 
     153    end 
     154    def post(*method_names) 
     155      method_names.flatten.each do |method| 
     156        @klass.class_eval <<-end_eval 
     157          def #{method}(url_options = {}) 
     158            returning(connection.post("\#{self.class.prefix(prefix_options)}\#{self.class.collection_name}.xml/new;#{method}", to_xml)) do |response| 
     159              self.id = id_from_response(response) 
     160            end 
     161          end 
     162        end_eval 
     163      end 
     164    end 
     165  end 
     166end 
  • 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