Browse files

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
  • Loading branch information...
1 parent e278b72 commit 7749c9c2200ad190e3f9935c27f09ec9b95227f2 @technoweenie technoweenie committed Sep 1, 2006
View
27 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]
View
117 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,76 +21,135 @@ 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
case method_name.last
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
View
11 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
View
109 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("<person><name>Matz</name><id type='integer'>1</id></person>"),
- ActiveResource::Request.new(:get, "/people/2.xml") => ActiveResource::Response.new("<person><name>David</name><id type='integer'>2</id></person>"),
- 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(
- "<people><person><name>Matz</name><id type='integer'>1</id></person><person><name>David</name><id type='integer'>2</id></person></people>"
- )
- )
+ @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", "<people>#{@matz}#{@david}</people>"
+ mock.get "/people/1/addresses.xml", "<addresses>#{@addy}</addresses>"
+ 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,18 +102,50 @@ 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"
assert_kind_of Person, matz
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
View
2 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
View
4 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
View
34 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

0 comments on commit 7749c9c

Please sign in to comment.