提交 7749c9c2 编写于 作者: R Rick Olson

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
上级 e278b72b
*SVN* *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] * 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] * Initial checkin: object-oriented client for restful HTTP resources which follow the Rails convention. [DHH]
...@@ -3,12 +3,10 @@ ...@@ -3,12 +3,10 @@
module ActiveResource module ActiveResource
class Base class Base
class << self class << self
def site=(site) attr_reader :site
@@site = site.is_a?(URI) ? site : URI.parse(site)
end
def site def site=(site)
@@site @site = site.is_a?(URI) ? site : URI.parse(site)
end end
def connection(refresh = false) def connection(refresh = false)
...@@ -23,66 +21,125 @@ def element_name ...@@ -23,66 +21,125 @@ def element_name
def collection_name def collection_name
element_name.pluralize element_name.pluralize
end 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) def primary_key
"/#{collection_name}/#{id}.xml" set_primary_key 'id'
end end
def collection_path def set_primary_key(value)
"/#{collection_name}.xml" class << self ; attr_reader :primary_key ; end
@primary_key = value
end end
# Person.find(1) # => GET /people/1.xml
# StreetAddress.find(1, :person_id => 1) # => GET /people/1/street_addresses/1.xml
def find(*arguments) def find(*arguments)
scope = arguments.slice!(0) scope = arguments.slice!(0)
options = arguments.slice!(0) || {}
case scope case scope
when Fixnum when :all then find_every(options)
# { :person => person1 } when :first then find_every(options).first
new(connection.get(element_path(scope)).values.first) else find_single(scope, options)
when :all
# { :people => { :person => [ person1, person2 ] } }
connection.get(collection_path).values.first.values.first.collect { |element| new(element) }
when :first
find(:all, *arguments).first
end end
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 end
attr_accessor :attributes attr_accessor :attributes
attr_accessor :prefix_options
def initialize(attributes = {}) def initialize(attributes = {}, prefix_options = {})
@attributes = attributes @attributes = attributes
@prefix_options = prefix_options
end end
def new_resource?
id.nil?
end
def id def id
attributes["id"] attributes[self.class.primary_key]
end end
def id=(id) def id=(id)
attributes["id"] = id attributes[self.class.primary_key] = id
end end
def save def save
update new_resource? ? create : update
end end
def destroy def destroy
connection.delete(self.class.element_path(id)) connection.delete(self.class.element_path(id, prefix_options)[0..-5])
end end
def to_xml def to_xml
attributes.to_xml(:root => self.class.element_name) attributes.to_xml(:root => self.class.element_name)
end 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 protected
def connection(refresh = false) def connection(refresh = false)
self.class.connection(refresh) self.class.connection(refresh)
end end
def update 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 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) def method_missing(method_symbol, *arguments)
method_name = method_symbol.to_s method_name = method_symbol.to_s
...@@ -90,9 +147,9 @@ def method_missing(method_symbol, *arguments) ...@@ -90,9 +147,9 @@ def method_missing(method_symbol, *arguments)
when "=" when "="
attributes[method_name.first(-1)] = arguments.first attributes[method_name.first(-1)] = arguments.first
when "?" when "?"
# TODO attributes[method_name.first(-1)] == true
else else
attributes[method_name] || super attributes.has_key?(method_name) ? attributes[method_name] : super
end end
end end
end end
......
...@@ -33,6 +33,11 @@ class << self ...@@ -33,6 +33,11 @@ class << self
def requests def requests
@@requests ||= [] @@requests ||= []
end end
def default_header
class << self ; attr_reader :default_header end
@default_header = { 'Content-Type' => 'application/xml' }
end
end end
def initialize(site) def initialize(site)
...@@ -44,15 +49,15 @@ def get(path) ...@@ -44,15 +49,15 @@ def get(path)
end end
def delete(path) def delete(path)
request(:delete, path) request(:delete, path, self.class.default_header)
end end
def put(path, body = '') def put(path, body = '')
request(:put, path, body) request(:put, path, body, self.class.default_header)
end end
def post(path, body = '') def post(path, body = '')
request(:post, path, body) request(:post, path, body, self.class.default_header)
end end
private private
......
require "#{File.dirname(__FILE__)}/abstract_unit" require "#{File.dirname(__FILE__)}/abstract_unit"
require "fixtures/person" require "fixtures/person"
require "fixtures/street_address"
class BaseTest < Test::Unit::TestCase class BaseTest < Test::Unit::TestCase
def setup def setup
ActiveResource::HttpMock.respond_to( @matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
ActiveResource::Request.new(:get, "/people/1.xml") => ActiveResource::Response.new("<person><name>Matz</name><id type='integer'>1</id></person>"), @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
ActiveResource::Request.new(:get, "/people/2.xml") => ActiveResource::Response.new("<person><name>David</name><id type='integer'>2</id></person>"), @addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address')
ActiveResource::Request.new(:put, "/people/1.xml") => ActiveResource::Response.new({}, 200), ActiveResource::HttpMock.respond_to do |mock|
ActiveResource::Request.new(:delete, "/people/1.xml") => ActiveResource::Response.new({}, 200), mock.get "/people/1.xml", @matz
ActiveResource::Request.new(:delete, "/people/2.xml") => ActiveResource::Response.new({}, 400), mock.get "/people/2.xml", @david
ActiveResource::Request.new(:post, "/people.xml") => ActiveResource::Response.new({}, 200), mock.put "/people/1", nil, 204
ActiveResource::Request.new(:get, "/people/99.xml") => ActiveResource::Response.new({}, 404), mock.delete "/people/1", nil, 200
ActiveResource::Request.new(:get, "/people.xml") => ActiveResource::Response.new( mock.delete "/people/2", nil, 400
"<people><person><name>Matz</name><id type='integer'>1</id></person><person><name>David</name><id type='integer'>2</id></person></people>" 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 end
...@@ -33,12 +45,47 @@ def test_collection_name ...@@ -33,12 +45,47 @@ def test_collection_name
assert_equal "people", Person.collection_name assert_equal "people", Person.collection_name
end 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 def test_find_by_id
matz = Person.find(1) matz = Person.find(1)
assert_kind_of Person, matz assert_kind_of Person, matz
assert_equal "Matz", matz.name assert_equal "Matz", matz.name
end 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 def test_find_all
all = Person.find(:all) all = Person.find(:all)
assert_equal 2, all.size assert_equal 2, all.size
...@@ -55,8 +102,21 @@ def test_find_first ...@@ -55,8 +102,21 @@ def test_find_first
def test_find_by_id_not_found def test_find_by_id_not_found
assert_raises(ActiveResource::ResourceNotFound) { Person.find(99) } 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 end
def test_update def test_update
matz = Person.find(:first) matz = Person.find(:first)
matz.name = "David" matz.name = "David"
...@@ -64,9 +124,28 @@ def test_update ...@@ -64,9 +124,28 @@ def test_update
assert_equal "David", matz.name assert_equal "David", matz.name
matz.save matz.save
end 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 def test_destroy
assert Person.find(1).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
end end
class Person < ActiveResource::Base class Person < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000/" self.site = "http://37s.sunrise.i:3000"
end end
\ No newline at end of file
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
...@@ -2,6 +2,20 @@ ...@@ -2,6 +2,20 @@
module ActiveResource module ActiveResource
class HttpMock 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 class << self
def requests def requests
@@requests ||= [] @@requests ||= []
...@@ -11,11 +25,12 @@ def responses ...@@ -11,11 +25,12 @@ def responses
@@responses ||= {} @@responses ||= {}
end end
def respond_to(pairs) def respond_to(pairs = {})
reset! reset!
pairs.each do |(path, response)| pairs.each do |(path, response)|
responses[path] = response responses[path] = response
end end
yield Responder.new(responses) if block_given?
end end
def reset! def reset!
...@@ -42,7 +57,7 @@ def initialize(site) ...@@ -42,7 +57,7 @@ def initialize(site)
class Request class Request
attr_accessor :path, :method, :body 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 @method, @path, @body = method, path, body
end end
...@@ -64,15 +79,24 @@ def hash ...@@ -64,15 +79,24 @@ def hash
end end
class Response class Response
attr_accessor :body, :code attr_accessor :body, :code, :headers
def initialize(body, code = 200) def initialize(body, code = 200, headers = nil)
@body, @code = body, code @body, @code, @headers = body, code, headers
end end
def success? def success?
(200..299).include?(code) (200..299).include?(code)
end end
def [](key)
headers[key]
end
def []=(key, value)
headers[key] = value
end
end end
class Connection class Connection
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册