From c918fbf14b96319412cb6195e7a8a3229613340c Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Mon, 4 Sep 2006 10:04:23 +0000 Subject: Deep hashes are converted into collections of resources. Class attribute writer methods. git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4985 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activeresource/CHANGELOG | 9 ++++ activeresource/lib/active_resource/base.rb | 60 ++++++++++++++++++++---- activeresource/test/base/load_test.rb | 65 ++++++++++++++++++++++++++ activeresource/test/fixtures/street_address.rb | 4 +- activeresource/test/http_mock.rb | 8 ++-- 5 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 activeresource/test/base/load_test.rb diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index 70f2a5eaee..2133824488 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -1,5 +1,14 @@ *SVN* +* Deep hashes are converted into collections of resources. [Jeremy Kemper] + Person.new :name => 'Bob', + :address => { :id => 1, :city => 'Portland' }, + :contacts => [{ :id => 1 }, { :id => 2 }] + Looks for Address and Contact resources and creates them if unavailable. + So clients can fetch a complex resource in a single request if you e.g. + render :xml => @person.to_xml(:include => [:address, :contacts]) + in your controller action. + * Major updates [Rick Olson] * Add full support for find/create/update/destroy diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index f722be4162..ec6cee19fa 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -7,6 +7,8 @@ module ActiveResource def site=(site) @site = site.is_a?(URI) ? site : URI.parse(site) + @connection = nil + @site end def connection(refresh = false) @@ -25,25 +27,28 @@ module ActiveResource def prefix(options={}) default = site.path default << '/' unless default[-1..-1] == '/' - set_prefix default + self.prefix = default prefix(options) end - def set_prefix(value = '/') + def prefix=(value = '/') prefix_call = value.gsub(/:\w+/) { |s| "\#{options[#{s}]}" } method_decl = %(def self.prefix(options={}) "#{prefix_call}" end) eval method_decl end + alias_method :set_prefix, :prefix= - def set_element_name(value) + def element_name=(value) class << self ; attr_reader :element_name ; end @element_name = value end + alias_method :set_element_name, :element_name= - def set_collection_name(value) + def collection_name=(value) class << self ; attr_reader :collection_name ; end @collection_name = value end + alias_method :set_collection_name, :collection_name= def element_path(id, options = {}) "#{prefix(options)}#{collection_name}/#{id}.xml" @@ -54,13 +59,14 @@ module ActiveResource end def primary_key - set_primary_key 'id' + self.primary_key = 'id' end - def set_primary_key(value) + def primary_key=(value) class << self ; attr_reader :primary_key ; end @primary_key = value end + alias_method :set_primary_key, :primary_key= # Person.find(1) # => GET /people/1.xml # StreetAddress.find(1, :person_id => 1) # => GET /people/1/street_addresses/1.xml @@ -91,7 +97,8 @@ module ActiveResource attr_accessor :prefix_options def initialize(attributes = {}, prefix_options = {}) - @attributes = attributes + @attributes = {} + self.load attributes @prefix_options = prefix_options end @@ -114,14 +121,34 @@ module ActiveResource def destroy connection.delete(self.class.element_path(id, prefix_options)[0..-5]) end - + def to_xml attributes.to_xml(:root => self.class.element_name) end # Reloads the attributes of this object from the remote web service. def reload - @attributes.update(self.class.find(self.id, @prefix_options).instance_variable_get(:@attributes)) + self.load self.class.find(id, @prefix_options).attributes + end + + # Manually load attributes from a hash. Recursively loads collections of + # resources. + def load(attributes) + attributes.each do |key, value| + @attributes[key.to_s] = + case value + when Array + resource = find_or_create_resource_for_collection(key) + value.map { |attrs| resource.new(attrs) } + when Hash + resource = find_or_create_resource_for(key) + resource.new(value) + when ActiveResource::Base + value.class.new(value.attributes) + else + value.dup rescue value + end + end self end @@ -140,6 +167,21 @@ module ActiveResource end end + private + def find_or_create_resource_for_collection(name) + find_or_create_resource_for(name.to_s.singularize) + end + + def find_or_create_resource_for(name) + resource_name = name.to_s.camelize + resource_name.constantize + rescue NameError + resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base)) + resource.prefix = self.class.prefix + resource.site = self.class.site + resource + end + def method_missing(method_symbol, *arguments) method_name = method_symbol.to_s diff --git a/activeresource/test/base/load_test.rb b/activeresource/test/base/load_test.rb new file mode 100644 index 0000000000..674f8456f0 --- /dev/null +++ b/activeresource/test/base/load_test.rb @@ -0,0 +1,65 @@ +require "#{File.dirname(__FILE__)}/../abstract_unit" +require "fixtures/person" +require "fixtures/street_address" + +class BaseLoadTest < Test::Unit::TestCase + def setup + @matz = { :id => 1, :name => 'Matz' } + @addys = [{ :id => 1, :street => '12345 Street' }, { :id => 2, :street => '67890 Street' }] + @deep = { :id => 1, :street => { + :id => 1, :state => { :id => 1, :name => 'Oregon', + :notable_rivers => [{ :id => 1, :name => 'Willamette' }, + { :id => 2, :name => 'Columbia', :rafted_by => @matz }] }}} + + @person = Person.new + end + + def test_load_simple_hash + assert_equal Hash.new, @person.attributes + assert_equal @matz.stringify_keys, @person.load(@matz).attributes + end + + def test_load_one_with_existing_resource + address = @person.load(:street_address => @addys.first).street_address + assert_kind_of StreetAddress, address + assert_equal @addys.first.stringify_keys, address.attributes + end + + def test_load_one_with_unknown_resource + address = silence_warnings { @person.load(:address => @addys.first).address } + assert_kind_of Person::Address, address + assert_equal @addys.first.stringify_keys, address.attributes + end + + def test_load_collection_with_existing_resource + addresses = @person.load(:street_addresses => @addys).street_addresses + addresses.each { |address| assert_kind_of StreetAddress, address } + assert_equal @addys.map(&:stringify_keys), addresses.map(&:attributes) + end + + def test_load_collection_with_unknown_resource + assert !Person.const_defined?(:Address), "Address shouldn't exist until autocreated" + addresses = silence_warnings { @person.load(:addresses => @addys).addresses } + assert Person.const_defined?(:Address), "Address should have been autocreated" + addresses.each { |address| assert_kind_of Person::Address, address } + assert_equal @addys.map(&:stringify_keys), addresses.map(&:attributes) + end + + def test_recursively_loaded_collections + person = @person.load(@deep) + assert_equal @deep[:id], person.id + + street = person.street + assert_kind_of Person::Street, street + assert_equal @deep[:street][:id], street.id + + state = street.state + assert_kind_of Person::Street::State, state + assert_equal @deep[:street][:state][:id], state.id + + rivers = state.notable_rivers + assert_kind_of Person::Street::State::NotableRiver, rivers.first + assert_equal @deep[:street][:state][:notable_rivers].first[:id], rivers.first.id + assert_equal @matz[:id], rivers.last.rafted_by.id + end +end diff --git a/activeresource/test/fixtures/street_address.rb b/activeresource/test/fixtures/street_address.rb index 84f20bbed6..94a86702b0 100644 --- a/activeresource/test/fixtures/street_address.rb +++ b/activeresource/test/fixtures/street_address.rb @@ -1,4 +1,4 @@ class StreetAddress < ActiveResource::Base self.site = "http://37s.sunrise.i:3000/people/:person_id/" - set_element_name 'address' -end \ No newline at end of file + self.element_name = 'address' +end diff --git a/activeresource/test/http_mock.rb b/activeresource/test/http_mock.rb index 75a54e71fe..40921f6b6b 100644 --- a/activeresource/test/http_mock.rb +++ b/activeresource/test/http_mock.rb @@ -101,8 +101,10 @@ module ActiveResource class Connection private - def http - @http ||= HttpMock.new(@site) + silence_warnings do + def http + @http ||= HttpMock.new(@site) + end end end -end \ No newline at end of file +end -- cgit v1.2.3