提交 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*
* 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]
......@@ -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
......
......@@ -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
......
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,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
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
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 @@
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
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册