From 7749c9c2200ad190e3f9935c27f09ec9b95227f2 Mon Sep 17 00:00:00 2001 From: Rick Olson Date: Fri, 1 Sep 2006 01:15:10 +0000 Subject: [PATCH] Major updates to ActiveResource, please see changelog and unit tests [Rick Olson] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4890 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activeresource/CHANGELOG | 27 ++++ activeresource/lib/active_resource/base.rb | 117 +++++++++++++----- .../lib/active_resource/connection.rb | 11 +- activeresource/test/base_test.rb | 109 +++++++++++++--- activeresource/test/fixtures/person.rb | 2 +- .../test/fixtures/street_address.rb | 4 + activeresource/test/http_mock.rb | 34 ++++- 7 files changed, 250 insertions(+), 54 deletions(-) create mode 100644 activeresource/test/fixtures/street_address.rb diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index 4b870e5db7..70f2a5eaee 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -1,5 +1,32 @@ *SVN* +* Major updates [Rick Olson] + + * Add full support for find/create/update/destroy + * Add support for specifying prefixes. + * Allow overriding of element_name, collection_name, and primary key + * Provide simpler HTTP mock interface for testing + + # rails routing code + map.resources :posts do |post| + post.resources :comments + end + + # ActiveResources + class Post < ActiveResource::Base + self.site = "http://37s.sunrise.i:3000/" + end + + class Comment < ActiveResource::Base + self.site = "http://37s.sunrise.i:3000/posts/:post_id/" + end + + @post = Post.find 5 + @comments = Comment.find :all, :post_id => @post.id + + @comment = Comment.new({:body => 'hello world'}, {:post_id => @post.id}) + @comment.save + * Base.site= accepts URIs. 200...400 are valid response codes. PUT and POST request bodies default to ''. [Jeremy Kemper] * Initial checkin: object-oriented client for restful HTTP resources which follow the Rails convention. [DHH] diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index 11064454d7..f722be4162 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -3,12 +3,10 @@ module ActiveResource class Base class << self - def site=(site) - @@site = site.is_a?(URI) ? site : URI.parse(site) - end + attr_reader :site - def site - @@site + def site=(site) + @site = site.is_a?(URI) ? site : URI.parse(site) end def connection(refresh = false) @@ -23,66 +21,125 @@ def element_name def collection_name element_name.pluralize end + + def prefix(options={}) + default = site.path + default << '/' unless default[-1..-1] == '/' + set_prefix default + prefix(options) + end + + def set_prefix(value = '/') + prefix_call = value.gsub(/:\w+/) { |s| "\#{options[#{s}]}" } + method_decl = %(def self.prefix(options={}) "#{prefix_call}" end) + eval method_decl + end + + def set_element_name(value) + class << self ; attr_reader :element_name ; end + @element_name = value + end + + def set_collection_name(value) + class << self ; attr_reader :collection_name ; end + @collection_name = value + end + + def element_path(id, options = {}) + "#{prefix(options)}#{collection_name}/#{id}.xml" + end + + def collection_path(options = {}) + "#{prefix(options)}#{collection_name}.xml" + end - def element_path(id) - "/#{collection_name}/#{id}.xml" + def primary_key + set_primary_key 'id' end - def collection_path - "/#{collection_name}.xml" + def set_primary_key(value) + class << self ; attr_reader :primary_key ; end + @primary_key = value end + # Person.find(1) # => GET /people/1.xml + # StreetAddress.find(1, :person_id => 1) # => GET /people/1/street_addresses/1.xml def find(*arguments) - scope = arguments.slice!(0) + scope = arguments.slice!(0) + options = arguments.slice!(0) || {} case scope - when Fixnum - # { :person => person1 } - new(connection.get(element_path(scope)).values.first) - when :all - # { :people => { :person => [ person1, person2 ] } } - connection.get(collection_path).values.first.values.first.collect { |element| new(element) } - when :first - find(:all, *arguments).first + when :all then find_every(options) + when :first then find_every(options).first + else find_single(scope, options) end end + + private + # { :people => { :person => [ person1, person2 ] } } + def find_every(options) + connection.get(collection_path(options)).values.first.values.first.collect { |element| new(element, options) } + end + + # { :person => person1 } + def find_single(scope, options) + new(connection.get(element_path(scope, options)).values.first, options) + end end attr_accessor :attributes + attr_accessor :prefix_options - def initialize(attributes = {}) - @attributes = attributes + def initialize(attributes = {}, prefix_options = {}) + @attributes = attributes + @prefix_options = prefix_options end - + + def new_resource? + id.nil? + end + def id - attributes["id"] + attributes[self.class.primary_key] end def id=(id) - attributes["id"] = id + attributes[self.class.primary_key] = id end def save - update + new_resource? ? create : update end def destroy - connection.delete(self.class.element_path(id)) + 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 + end + protected def connection(refresh = false) self.class.connection(refresh) end def update - connection.put(self.class.element_path(id), to_xml) + connection.put(self.class.element_path(id, prefix_options)[0..-5], to_xml) end - + + def create + returning connection.post(self.class.collection_path(prefix_options)[0..-5], to_xml) do |resp| + self.id = resp['Location'][/\/([^\/]*?)(\.\w+)?$/, 1] + end + end + def method_missing(method_symbol, *arguments) method_name = method_symbol.to_s @@ -90,9 +147,9 @@ def method_missing(method_symbol, *arguments) when "=" attributes[method_name.first(-1)] = arguments.first when "?" - # TODO + attributes[method_name.first(-1)] == true else - attributes[method_name] || super + attributes.has_key?(method_name) ? attributes[method_name] : super end end end diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb index cc98645fdc..22689444b2 100644 --- a/activeresource/lib/active_resource/connection.rb +++ b/activeresource/lib/active_resource/connection.rb @@ -33,6 +33,11 @@ class << self def requests @@requests ||= [] end + + def default_header + class << self ; attr_reader :default_header end + @default_header = { 'Content-Type' => 'application/xml' } + end end def initialize(site) @@ -44,15 +49,15 @@ def get(path) end def delete(path) - request(:delete, path) + request(:delete, path, self.class.default_header) end def put(path, body = '') - request(:put, path, body) + request(:put, path, body, self.class.default_header) end def post(path, body = '') - request(:post, path, body) + request(:post, path, body, self.class.default_header) end private diff --git a/activeresource/test/base_test.rb b/activeresource/test/base_test.rb index e4839613ee..97f2ea3d01 100644 --- a/activeresource/test/base_test.rb +++ b/activeresource/test/base_test.rb @@ -1,20 +1,32 @@ require "#{File.dirname(__FILE__)}/abstract_unit" require "fixtures/person" +require "fixtures/street_address" class BaseTest < Test::Unit::TestCase def setup - ActiveResource::HttpMock.respond_to( - ActiveResource::Request.new(:get, "/people/1.xml") => ActiveResource::Response.new("Matz1"), - ActiveResource::Request.new(:get, "/people/2.xml") => ActiveResource::Response.new("David2"), - ActiveResource::Request.new(:put, "/people/1.xml") => ActiveResource::Response.new({}, 200), - ActiveResource::Request.new(:delete, "/people/1.xml") => ActiveResource::Response.new({}, 200), - ActiveResource::Request.new(:delete, "/people/2.xml") => ActiveResource::Response.new({}, 400), - ActiveResource::Request.new(:post, "/people.xml") => ActiveResource::Response.new({}, 200), - ActiveResource::Request.new(:get, "/people/99.xml") => ActiveResource::Response.new({}, 404), - ActiveResource::Request.new(:get, "/people.xml") => ActiveResource::Response.new( - "Matz1David2" - ) - ) + @matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person') + @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person') + @addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address') + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/people/1.xml", @matz + mock.get "/people/2.xml", @david + mock.put "/people/1", nil, 204 + mock.delete "/people/1", nil, 200 + mock.delete "/people/2", nil, 400 + mock.post "/people", nil, 201, 'Location' => '/people/5.xml' + mock.get "/people/99.xml", nil, 404 + mock.get "/people.xml", "#{@matz}#{@david}" + mock.get "/people/1/addresses.xml", "#{@addy}" + mock.get "/people/1/addresses/1.xml", @addy + mock.put "/people/1/addresses/1", nil, 204 + mock.delete "/people/1/addresses/1", nil, 200 + mock.post "/people/1/addresses", nil, 201, 'Location' => '/people/1/addresses/5' + mock.get "/people//addresses.xml", nil, 404 + mock.get "/people//addresses/1.xml", nil, 404 + mock.put "/people//addresses/1", nil, 404 + mock.delete "/people//addresses/1", nil, 404 + mock.post "/people//addresses", nil, 404 + end end @@ -33,12 +45,47 @@ def test_collection_name assert_equal "people", Person.collection_name end + def test_collection_path + assert_equal '/people.xml', Person.collection_path + end + + def test_custom_element_path + assert_equal '/people/1/addresses/1.xml', StreetAddress.element_path(1, :person_id => 1) + end + + def test_custom_collection_path + assert_equal '/people/1/addresses.xml', StreetAddress.collection_path(:person_id => 1) + end + + def test_custom_element_name + assert_equal 'address', StreetAddress.element_name + end + + def test_custom_collection_name + assert_equal 'addresses', StreetAddress.collection_name + end + + def test_prefix + assert_equal "/", Person.prefix + end + + def test_custom_prefix + assert_equal '/people//', StreetAddress.prefix + assert_equal '/people/1/', StreetAddress.prefix(:person_id => 1) + end + def test_find_by_id matz = Person.find(1) assert_kind_of Person, matz assert_equal "Matz", matz.name end + def test_find_by_id_with_custom_prefix + addy = StreetAddress.find(1, :person_id => 1) + assert_kind_of StreetAddress, addy + assert_equal '12345 Street', addy.street + end + def test_find_all all = Person.find(:all) assert_equal 2, all.size @@ -55,8 +102,21 @@ def test_find_first def test_find_by_id_not_found assert_raises(ActiveResource::ResourceNotFound) { Person.find(99) } + assert_raises(ActiveResource::ResourceNotFound) { StreetAddress.find(1) } + end + + def test_create + rick = Person.new + rick.save + assert_equal '5', rick.id + end + + def test_create_with_custom_prefix + matzs_house = StreetAddress.new({}, {:person_id => 1}) + matzs_house.save + assert_equal '5', matzs_house.id end - + def test_update matz = Person.find(:first) matz.name = "David" @@ -64,9 +124,28 @@ def test_update assert_equal "David", matz.name matz.save end - + + def test_update_with_custom_prefix + addy = StreetAddress.find(1, :person_id => 1) + addy.street = "54321 Street" + assert_kind_of StreetAddress, addy + assert_equal "54321 Street", addy.street + addy.save + end + def test_destroy assert Person.find(1).destroy - assert_raises(ActiveResource::ClientError) { Person.find(2).destroy } + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/people/1.xml", nil, 404 + end + assert_raises(ActiveResource::ResourceNotFound) { Person.find(1).destroy } + end + + def test_destroy_with_custom_prefix + assert StreetAddress.find(1, :person_id => 1).destroy + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/people/1/addresses/1.xml", nil, 404 + end + assert_raises(ActiveResource::ResourceNotFound) { StreetAddress.find(1, :person_id => 1).destroy } end end diff --git a/activeresource/test/fixtures/person.rb b/activeresource/test/fixtures/person.rb index 4914863230..8e5768586a 100644 --- a/activeresource/test/fixtures/person.rb +++ b/activeresource/test/fixtures/person.rb @@ -1,3 +1,3 @@ class Person < ActiveResource::Base - self.site = "http://37s.sunrise.i:3000/" + self.site = "http://37s.sunrise.i:3000" end \ No newline at end of file diff --git a/activeresource/test/fixtures/street_address.rb b/activeresource/test/fixtures/street_address.rb new file mode 100644 index 0000000000..84f20bbed6 --- /dev/null +++ b/activeresource/test/fixtures/street_address.rb @@ -0,0 +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 diff --git a/activeresource/test/http_mock.rb b/activeresource/test/http_mock.rb index a4bc7e7cb2..75a54e71fe 100644 --- a/activeresource/test/http_mock.rb +++ b/activeresource/test/http_mock.rb @@ -2,6 +2,20 @@ module ActiveResource class HttpMock + class Responder + def initialize(responses) + @responses = responses + end + + for method in [ :post, :put, :get, :delete ] + module_eval <<-EOE + def #{method}(path, body = nil, status = 200, headers = {}) + @responses[Request.new(:#{method}, path, nil)] = Response.new(body || {}, status, headers) + end + EOE + end + end + class << self def requests @@requests ||= [] @@ -11,11 +25,12 @@ def responses @@responses ||= {} end - def respond_to(pairs) + def respond_to(pairs = {}) reset! pairs.each do |(path, response)| responses[path] = response end + yield Responder.new(responses) if block_given? end def reset! @@ -42,7 +57,7 @@ def initialize(site) class Request attr_accessor :path, :method, :body - def initialize(method, path, body = nil) + def initialize(method, path, body = nil, headers = nil) @method, @path, @body = method, path, body end @@ -64,15 +79,24 @@ def hash end class Response - attr_accessor :body, :code + attr_accessor :body, :code, :headers - def initialize(body, code = 200) - @body, @code = body, code + def initialize(body, code = 200, headers = nil) + @body, @code, @headers = body, code, headers end def success? (200..299).include?(code) end + + def [](key) + headers[key] + end + + def []=(key, value) + headers[key] = value + end + end class Connection -- GitLab