提交 f1637bf2 编写于 作者: P Prem Sichanugrist

Remove Active Resource source files from the repository

Dear Active Resource,

It's not that I hate you or anything, but you didn't get much attention lately. There're so many alternatives out there, and I think people have made their choice to use them than you. I think it's time for you to have a big rest, peacefully in this Git repository.

I will miss you,

@sikachu.
上级 a85714a6
......@@ -13,7 +13,7 @@ task :build => "all:build"
desc "Release all gems to gemcutter and create a tag"
task :release => "all:release"
PROJECTS = %w(activesupport activemodel actionpack actionmailer activeresource activerecord railties)
PROJECTS = %w(activesupport activemodel actionpack actionmailer activerecord railties)
desc 'Run all tests by default'
task :default => %w(test test:isolated)
......@@ -109,11 +109,6 @@ RDoc::Task.new do |rdoc|
rdoc.rdoc_files.include('activerecord/lib/active_record/**/*.rb')
rdoc.rdoc_files.exclude('activerecord/lib/active_record/vendor/*')
rdoc.rdoc_files.include('activeresource/README.rdoc')
rdoc.rdoc_files.include('activeresource/CHANGELOG.md')
rdoc.rdoc_files.include('activeresource/lib/active_resource.rb')
rdoc.rdoc_files.include('activeresource/lib/active_resource/*')
rdoc.rdoc_files.include('actionpack/README.rdoc')
rdoc.rdoc_files.include('actionpack/CHANGELOG.md')
rdoc.rdoc_files.include('actionpack/lib/abstract_controller/**/*.rb')
......@@ -157,7 +152,6 @@ task :update_versions do
"activemodel" => "ActiveModel",
"actionpack" => "ActionPack",
"actionmailer" => "ActionMailer",
"activeresource" => "ActiveResource",
"activerecord" => "ActiveRecord",
"railties" => "Rails"
}
......
......@@ -2,8 +2,8 @@
module ActionController
# The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or
# Active Resources or pretty much any other model type that has an id. These patterns are then used to try elevate
# the view actions to a higher logical level. Example:
# pretty much any other model type that has an id. These patterns are then used to try elevate the view actions to
# a higher logical level. Example:
#
# # routes
# resources :posts
......
......@@ -60,7 +60,7 @@ def optimize_routes_generation? #:nodoc:
#
# ==== Relying on named routes
#
# Passing a record (like an Active Record or Active Resource) instead of a Hash as the options parameter will
# Passing a record (like an Active Record) instead of a Hash as the options parameter will
# trigger the named route for that record. The lookup will happen on the name of the class. So passing a
# Workshop object will attempt to use the +workshop_path+ route. If you have a nested route, such as
# +admin_workshop_path+ you'll have to call that explicitly (it's impossible for +url_for+ to guess that route).
......
......@@ -5,7 +5,7 @@
s.name = 'activemodel'
s.version = version
s.summary = 'A toolkit for building modeling frameworks (part of Rails).'
s.description = 'A toolkit for building modeling frameworks like Active Record and Active Resource. Rich support for attributes, callbacks, validations, observers, serialization, internationalization, and testing.'
s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, observers, serialization, internationalization, and testing.'
s.required_ruby_version = '>= 1.9.3'
......
## Rails 4.0.0 (unreleased) ##
* Adds support for PATCH requests. *dlee*
## Rails 3.2.2 (March 1, 2012) ##
* No changes.
## Rails 3.2.1 (January 26, 2012) ##
* Documentation fixes.
## Rails 3.2.0 (January 20, 2012) ##
* Redirect responses: 303 See Other and 307 Temporary Redirect now behave like
301 Moved Permanently and 302 Found. GH #3302.
*Jim Herz*
## Rails 3.1.4 (March 1, 2012) ##
* No changes
## Rails 3.1.3 (November 20, 2011) ##
* No changes
## Rails 3.1.2 (November 18, 2011) ##
* No changes
## Rails 3.1.1 (October 7, 2011) ##
* No changes.
## Rails 3.1.0 (August 30, 2011) ##
* The default format has been changed to JSON for all requests. If you want to continue to use XML you will need to set `self.format = :xml` in the class. eg.
class User < ActiveResource::Base self.format = :xml
end
## Rails 3.0.12 (March 1, 2012) ##
* No changes.
## Rails 3.0.11 (November 18, 2011) ##
* No changes.
## Rails 3.0.10 (August 16, 2011) ##
* No changes.
## Rails 3.0.9 (June 16, 2011) ##
* No changes.
## Rails 3.0.8 (June 7, 2011) ##
* No Changes
## Rails 3.0.7 (April 18, 2011) ##
* No changes.
## Rails 3.0.6 (April 5, 2011) ##
* No changes.
## Rails 3.0.5 (February 26, 2011) ##
* No changes.
## Rails 3.0.4 (February 8, 2011) ##
* No changes.
## Rails 3.0.3 (November 16, 2010) ##
* No changes.
## Rails 3.0.2 (November 15, 2010) ##
* No changes
## Rails 3.0.1 (October 15, 2010) ##
* No Changes, just a version bump.
## Rails 3.0.0 (August 29, 2010) ##
* JSON: set Base.include_root_in_json = true to include a root value in the JSON: {"post": {"title": ...}}. Mirrors the Active Record option. *Santiago Pastorino*
* Add support for errors in JSON format. #1956 *Fabien Jakimowicz*
* Recognizes 410 as Resource Gone. #2316 *Jordan Brough, Jatinder Singh*
* More thorough SSL support. #2370 *Roy Nicholson*
* HTTP proxy support. #2133 *Marshall Huss, Sébastien Dabet*
## 2.3.2 Final (March 15, 2009) ##
* Nothing new, just included in 2.3.2
## 2.2.1 RC2 (November 14th, 2008) ##
* Fixed that ActiveResource#post would post an empty string when it shouldn't be posting anything #525 *Paolo Angelini*
## 2.2.0 RC1 (October 24th, 2008) ##
* Add ActiveResource::Base#to_xml and ActiveResource::Base#to_json. #1011 *Rasik Pandey, Cody Fauser*
* Add ActiveResource::Base.find(:last). [#754 state:resolved] (Adrian Mugnolo)
* Fixed problems with the logger used if the logging string included %'s [#840 state:resolved] (Jamis Buck)
* Fixed Base#exists? to check status code as integer [#299 state:resolved] (Wes Oldenbeuving)
## 2.1.0 (May 31st, 2008) ##
* Fixed response logging to use length instead of the entire thing (seangeo) *#27*
* Fixed that to_param should be used and honored instead of hardcoding the id #11406 *gspiers*
* Improve documentation. *Ryan Bigg, Jan De Poorter, Cheah Chu Yeow, Xavier Shay, Jack Danger Canty, Emilio Tagua, Xavier Noria, Sunny Ripert*
* Use HEAD instead of GET in exists? *bscofield*
* Fix small documentation typo. Closes #10670 *Luca Guidi*
* find_or_create_resource_for handles module nesting. #10646 *xavier*
* Allow setting ActiveResource::Base#format before #site. *Rick Olson*
* Support agnostic formats when calling custom methods. Closes #10635 *joerichsen*
* Document custom methods. #10589 *Cheah Chu Yeow*
* Ruby 1.9 compatibility. *Jeremy Kemper*
## 2.0.2 (December 16th, 2007) ##
* Added more specific exceptions for 400, 401, and 403 (all descending from ClientError so existing rescues will work) #10326 *trek*
* Correct empty response handling. #10445 *seangeo*
## 2.0.1 (December 7th, 2007) ##
* Don't cache net/http object so that ActiveResource is more thread-safe. Closes #10142 *kou*
* Update XML documentation examples to include explicit type attributes. Closes #9754 *Josh Susser*
* Added one-off declarations of mock behavior [David Heinemeier Hansson]. Example:
Before:
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/1.xml", {}, "<person><name>David</name></person>"
end
Now:
ActiveResource::HttpMock.respond_to.get "/people/1.xml", {}, "<person><name>David</name></person>"
* Added ActiveResource.format= which defaults to :xml but can also be set to :json [David Heinemeier Hansson]. Example:
class Person < ActiveResource::Base
self.site = "http://app/"
self.format = :json
end
person = Person.find(1) # => GET http://app/people/1.json
person.name = "David"
person.save # => PUT http://app/people/1.json {name: "David"}
Person.format = :xml
person.name = "Mary"
person.save # => PUT http://app/people/1.json <person><name>Mary</name></person>
* Fix reload error when path prefix is used. #8727 *Ian Warshak*
* Remove ActiveResource::Struct because it hasn't proven very useful. Creating a new ActiveResource::Base subclass is often less code and always clearer. #8612 *Josh Peek*
* Fix query methods on resources. *Cody Fauser*
* pass the prefix_options to the instantiated record when using find without a specific id. Closes #8544 *Eloy Duran*
* Recognize and raise an exception on 405 Method Not Allowed responses. #7692 *Josh Peek*
* Handle string and symbol param keys when splitting params into prefix params and query params.
Comment.find(:all, :params => { :article_id => 5, :page => 2 }) or Comment.find(:all, :params => { 'article_id' => 5, :page => 2 })
* Added find-one with symbol [David Heinemeier Hansson]. Example: Person.find(:one, :from => :leader) # => GET /people/leader.xml
* BACKWARDS INCOMPATIBLE: Changed the finder API to be more extensible with :params and more strict usage of scopes [David Heinemeier Hansson]. Changes:
Person.find(:all, :title => "CEO") ...becomes: Person.find(:all, :params => { :title => "CEO" })
Person.find(:managers) ...becomes: Person.find(:all, :from => :managers)
Person.find("/companies/1/manager.xml") ...becomes: Person.find(:one, :from => "/companies/1/manager.xml")
* Add support for setting custom headers per Active Resource model *Rick Olson*
class Project
headers['X-Token'] = 'foo'
end
\# makes the GET request with the custom X-Token header
Project.find(:all)
* Added find-by-path options to ActiveResource::Base.find [David Heinemeier Hansson]. Examples:
employees = Person.find(:all, :from => "/companies/1/people.xml") # => GET /companies/1/people.xml
manager = Person.find("/companies/1/manager.xml") # => GET /companies/1/manager.xml
* Added support for using classes from within a single nested module [David Heinemeier Hansson]. Example:
module Highrise
class Note < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000"
end
class Comment < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000"
end
end
assert_kind_of Highrise::Comment, Note.find(1).comments.first
* Added load_attributes_from_response as a way of loading attributes from other responses than just create *David Heinemeier Hansson*
class Highrise::Task < ActiveResource::Base
def complete
load_attributes_from_response(post(:complete))
end
end
...will set "done_at" when complete is called.
* Added support for calling custom methods #6979 *rwdaigle*
Person.find(:managers) # => GET /people/managers.xml
Kase.find(1).post(:close) # => POST /kases/1/close.xml
* Remove explicit prefix_options parameter for ActiveResource::Base#initialize. *Rick Olson*
ActiveResource splits the prefix_options from it automatically.
* Allow ActiveResource::Base.delete with custom prefix. *Rick Olson*
* Add ActiveResource::Base#dup *Rick Olson*
* Fixed constant warning when fetching the same object multiple times *David Heinemeier Hansson*
* Added that saves which get a body response (and not just a 201) will use that response to update themselves *David Heinemeier Hansson*
* Disregard namespaces from the default element name, so Highrise::Person will just try to fetch from "/people", not "/highrise/people" *David Heinemeier Hansson*
* Allow array and hash query parameters. #7756 *Greg Spurrier*
* Loading a resource preserves its prefix_options. #7353 *Ryan Daigle*
* Carry over the convenience of #create from ActiveRecord. Closes #7340. *Ryan Daigle*
* Increase ActiveResource::Base test coverage. Closes #7173, #7174 *Rich Collins*
* Interpret 422 Unprocessable Entity as ResourceInvalid. #7097 *dkubb*
* Mega documentation patches. #7025, #7069 *rwdaigle*
* Base.exists?(id, options) and Base#exists? check whether the resource is found. #6970 *rwdaigle*
* Query string support. *untext, Jeremy Kemper*
# GET /forums/1/topics.xml?sort=created_at
Topic.find(:all, :forum_id => 1, :sort => 'created_at')
* Base#==, eql?, and hash methods. == returns true if its argument is identical to self or if it's an instance of the same class, is not new?, and has the same id. eql? is an alias for ==. hash delegates to id. *Jeremy Kemper*
* Allow subclassed resources to share the site info *Rick Olson, Jeremy Kemper*
d class BeastResource < ActiveResource::Base
self.site = 'http://beast.caboo.se'
end
class Forum < BeastResource
# taken from BeastResource
# self.site = 'http://beast.caboo.se'
end
class Topic < BeastResource
self.site += '/forums/:forum_id'
end
* Fix issues with ActiveResource collection handling. Closes #6291. *bmilekic*
* Use attr_accessor_with_default to dry up attribute initialization. References #6538. *Stuart Halloway*
* Add basic logging support for logging outgoing requests. *Jamis Buck*
* Add Base.delete for deleting resources without having to instantiate them first. *Jamis Buck*
* Make #save behavior mimic AR::Base#save (true on success, false on failure). *Jamis Buck*
* Add Basic HTTP Authentication to ActiveResource (closes #6305). *jonathan*
* Extracted #id_from_response as an entry point for customizing how a created resource gets its own ID.
By default, it extracts from the Location response header.
* Optimistic locking: raise ActiveResource::ResourceConflict on 409 Conflict response. *Jeremy Kemper*
# Example controller action
def update
@person.save!
rescue ActiveRecord::StaleObjectError
render :xml => @person.reload.to_xml, :status => '409 Conflict'
end
* Basic validation support *Rick Olson*
Parses the xml response of ActiveRecord::Errors#to_xml with a similar interface to ActiveRecord::Errors.
render :xml => @person.errors.to_xml, :status => '400 Validation Error'
* 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
* 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. *David Heinemeier Hansson*
Copyright (c) 2006-2012 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
= Active Resource
Active Resource (ARes) connects business objects and Representational State Transfer (REST)
web services. It implements object-relational mapping for REST web services to provide transparent
proxying capabilities between a client (ActiveResource) and a RESTful service (which is provided by Simply RESTful routing
in ActionController::Resources).
== Philosophy
Active Resource attempts to provide a coherent wrapper object-relational mapping for REST
web services. It follows the same philosophy as Active Record, in that one of its prime aims
is to reduce the amount of code needed to map to these resources. This is made possible
by relying on a number of code- and protocol-based conventions that make it easy for Active Resource
to infer complex relations and structures. These conventions are outlined in detail in the documentation
for ActiveResource::Base.
== Overview
Model classes are mapped to remote REST resources by Active Resource much the same way Active Record maps model classes to database
tables. When a request is made to a remote resource, a REST XML request is generated, transmitted, and the result
received and serialized into a usable Ruby object.
== Download and installation
The latest version of Active Support can be installed with RubyGems:
% [sudo] gem install activeresource
Source code can be downloaded as part of the Rails project on GitHub
* https://github.com/rails/rails/tree/master/activeresource
=== Configuration and Usage
Putting Active Resource to use is very similar to Active Record. It's as simple as creating a model class
that inherits from ActiveResource::Base and providing a <tt>site</tt> class variable to it:
class Person < ActiveResource::Base
self.site = "http://api.people.com:3000"
end
Now the Person class is REST enabled and can invoke REST services very similarly to how Active Record invokes
life cycle methods that operate against a persistent store.
# Find a person with id = 1
ryan = Person.find(1)
Person.exists?(1) # => true
As you can see, the methods are quite similar to Active Record's methods for dealing with database
records. But rather than dealing directly with a database record, you're dealing with HTTP resources (which may or may not be database records).
==== Protocol
Active Resource is built on a standard XML format for requesting and submitting resources over HTTP. It mirrors the RESTful routing
built into Action Controller but will also work with any other REST service that properly implements the protocol.
REST uses HTTP, but unlike "typical" web applications, it makes use of all the verbs available in the HTTP specification:
* GET requests are used for finding and retrieving resources.
* POST requests are used to create new resources.
* PUT requests are used to update existing resources.
* DELETE requests are used to delete resources.
For more information on how this protocol works with Active Resource, see the ActiveResource::Base documentation;
for more general information on REST web services, see the article here[http://en.wikipedia.org/wiki/Representational_State_Transfer].
==== Find
Find requests use the GET method and expect the XML form of whatever resource/resources is/are being requested. So,
for a request for a single element, the XML of that item is expected in response:
# Expects a response of
#
# <person><id type="integer">1</id><attribute1>value1</attribute1><attribute2>..</attribute2></person>
#
# for GET http://api.people.com:3000/people/1.xml
#
ryan = Person.find(1)
The XML document that is received is used to build a new object of type Person, with each
XML element becoming an attribute on the object.
ryan.is_a? Person # => true
ryan.attribute1 # => 'value1'
Any complex element (one that contains other elements) becomes its own object:
# With this response:
#
# <person><id>1</id><attribute1>value1</attribute1><complex><attribute2>value2</attribute2></complex></person>
#
# for GET http://api.people.com:3000/people/1.xml
#
ryan = Person.find(1)
ryan.complex # => <Person::Complex::xxxxx>
ryan.complex.attribute2 # => 'value2'
Collections can also be requested in a similar fashion
# Expects a response of
#
# <people type="array">
# <person><id type="integer">1</id><first>Ryan</first></person>
# <person><id type="integer">2</id><first>Jim</first></person>
# </people>
#
# for GET http://api.people.com:3000/people.xml
#
people = Person.all
people.first # => <Person::xxx 'first' => 'Ryan' ...>
people.last # => <Person::xxx 'first' => 'Jim' ...>
==== Create
Creating a new resource submits the XML form of the resource as the body of the request and expects
a 'Location' header in the response with the RESTful URL location of the newly created resource. The
id of the newly created resource is parsed out of the Location response header and automatically set
as the id of the ARes object.
# <person><first>Ryan</first></person>
#
# is submitted as the body on
#
# POST http://api.people.com:3000/people.xml
#
# when save is called on a new Person object. An empty response is
# is expected with a 'Location' header value:
#
# Response (201): Location: http://api.people.com:3000/people/2
#
ryan = Person.new(:first => 'Ryan')
ryan.new? # => true
ryan.save # => true
ryan.new? # => false
ryan.id # => 2
==== Update
'save' is also used to update an existing resource and follows the same protocol as creating a resource
with the exception that no response headers are needed -- just an empty response when the update on the
server side was successful.
# <person><first>Ryan</first></person>
#
# is submitted as the body on
#
# PUT http://api.people.com:3000/people/1.xml
#
# when save is called on an existing Person object. An empty response is
# is expected with code (204)
#
ryan = Person.find(1)
ryan.first # => 'Ryan'
ryan.first = 'Rizzle'
ryan.save # => true
==== Delete
Destruction of a resource can be invoked as a class and instance method of the resource.
# A request is made to
#
# DELETE http://api.people.com:3000/people/1.xml
#
# for both of these forms. An empty response with
# is expected with response code (200)
#
ryan = Person.find(1)
ryan.destroy # => true
ryan.exists? # => false
Person.delete(2) # => true
Person.exists?(2) # => false
== License
Active Support is released under the MIT license:
* http://www.opensource.org/licenses/MIT
== Support
API documentation is at
* http://api.rubyonrails.org
Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
You can find more usage information in the ActiveResource::Base documentation.
#!/usr/bin/env rake
require 'rake/testtask'
require 'rake/packagetask'
require 'rubygems/package_task'
desc "Default Task"
task :default => [ :test ]
# Run the unit tests
Rake::TestTask.new { |t|
t.libs << "test"
t.pattern = 'test/**/*_test.rb'
t.warning = true
t.verbose = true
}
namespace :test do
task :isolated do
ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
activesupport_path = "#{File.dirname(__FILE__)}/../activesupport/lib"
Dir.glob("test/**/*_test.rb").all? do |file|
sh(ruby, '-w', "-Ilib:test:#{activesupport_path}", file)
end or raise "Failures"
end
end
spec = eval(File.read('activeresource.gemspec'))
Gem::PackageTask.new(spec) do |p|
p.gem_spec = spec
end
task :lines do
lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
FileList["lib/active_resource/**/*.rb"].each do |file_name|
next if file_name =~ /vendor/
f = File.open(file_name)
while line = f.gets
lines += 1
next if line =~ /^\s*$/
next if line =~ /^\s*#/
codelines += 1
end
puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
total_lines += lines
total_codelines += codelines
lines, codelines = 0, 0
end
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
end
# Publishing ------------------------------------------------------
desc "Release to gemcutter"
task :release => :package do
require 'rake/gemcutter'
Rake::Gemcutter::Tasks.new(spec).define
Rake::Task['gem:push'].invoke
end
version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = 'activeresource'
s.version = version
s.summary = 'REST modeling framework (part of Rails).'
s.description = 'REST on Rails. Wrap your RESTful web app with Ruby classes and work with them like Active Record models.'
s.required_ruby_version = '>= 1.9.3'
s.author = 'David Heinemeier Hansson'
s.email = 'david@loudthinking.com'
s.homepage = 'http://www.rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'examples/**/*', 'lib/**/*']
s.require_path = 'lib'
s.extra_rdoc_files = %w( README.rdoc )
s.rdoc_options.concat ['--main', 'README.rdoc']
s.add_dependency('activesupport', version)
s.add_dependency('activemodel', version)
end
require 'rubygems'
require 'active_resource'
require 'benchmark'
TIMES = (ENV['N'] || 10_000).to_i
# deep nested resource
attrs = {
:id => 1,
:name => 'Luis',
:age => 21,
:friends => [
{
:name => 'JK',
:age => 24,
:colors => ['red', 'green', 'blue'],
:brothers => [
{
:name => 'Mateo',
:age => 35,
:children => [{ :name => 'Edith', :age => 5 }, { :name => 'Martha', :age => 4 }]
},
{
:name => 'Felipe',
:age => 33,
:children => [{ :name => 'Bryan', :age => 1 }, { :name => 'Luke', :age => 0 }]
}
]
},
{
:name => 'Eduardo',
:age => 20,
:colors => [],
:brothers => [
{
:name => 'Sebas',
:age => 23,
:children => [{ :name => 'Andres', :age => 0 }, { :name => 'Jorge', :age => 2 }]
},
{
:name => 'Elsa',
:age => 19,
:children => [{ :name => 'Natacha', :age => 1 }]
},
{
:name => 'Milena',
:age => 16,
:children => []
}
]
}
]
}
class Customer < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000"
end
module Nested
class Customer < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000"
end
end
Benchmark.bm(40) do |x|
x.report('Model.new (instantiation)') { TIMES.times { Customer.new } }
x.report('Nested::Model.new (instantiation)') { TIMES.times { Nested::Customer.new } }
x.report('Model.new (setting attributes)') { TIMES.times { Customer.new attrs } }
x.report('Nested::Model.new (setting attributes)') { TIMES.times { Nested::Customer.new attrs } }
end
#--
# Copyright (c) 2006-2012 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
activemodel_path = File.expand_path('../../../activemodel/lib', __FILE__)
$:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include?(activemodel_path)
require 'active_support'
require 'active_model'
require 'active_resource/version'
module ActiveResource
extend ActiveSupport::Autoload
autoload :Base
autoload :Connection
autoload :CustomMethods
autoload :Formats
autoload :HttpMock
autoload :Observing
autoload :Schema
autoload :Validations
end
此差异已折叠。
require 'active_support/core_ext/benchmark'
require 'active_support/core_ext/uri'
require 'active_support/core_ext/object/inclusion'
require 'net/https'
require 'date'
require 'time'
require 'uri'
module ActiveResource
# Class to handle connections to remote web services.
# This class is used by ActiveResource::Base to interface with REST
# services.
class Connection
HTTP_FORMAT_HEADER_NAMES = { :get => 'Accept',
:put => 'Content-Type',
:post => 'Content-Type',
:patch => 'Content-Type',
:delete => 'Accept',
:head => 'Accept'
}
attr_reader :site, :user, :password, :auth_type, :timeout, :proxy, :ssl_options
attr_accessor :format
class << self
def requests
@@requests ||= []
end
end
# The +site+ parameter is required and will set the +site+
# attribute to the URI for the remote resource service.
def initialize(site, format = ActiveResource::Formats::JsonFormat)
raise ArgumentError, 'Missing site URI' unless site
@user = @password = nil
self.site = site
self.format = format
end
# Set URI for remote service.
def site=(site)
@site = site.is_a?(URI) ? site : URI.parse(site)
@user = URI.parser.unescape(@site.user) if @site.user
@password = URI.parser.unescape(@site.password) if @site.password
end
# Set the proxy for remote service.
def proxy=(proxy)
@proxy = proxy.is_a?(URI) ? proxy : URI.parse(proxy)
end
# Sets the user for remote service.
def user=(user)
@user = user
end
# Sets the password for remote service.
def password=(password)
@password = password
end
# Sets the auth type for remote service.
def auth_type=(auth_type)
@auth_type = legitimize_auth_type(auth_type)
end
# Sets the number of seconds after which HTTP requests to the remote service should time out.
def timeout=(timeout)
@timeout = timeout
end
# Hash of options applied to Net::HTTP instance when +site+ protocol is 'https'.
def ssl_options=(opts={})
@ssl_options = opts
end
# Executes a GET request.
# Used to get (find) resources.
def get(path, headers = {})
with_auth { request(:get, path, build_request_headers(headers, :get, self.site.merge(path))) }
end
# Executes a DELETE request (see HTTP protocol documentation if unfamiliar).
# Used to delete resources.
def delete(path, headers = {})
with_auth { request(:delete, path, build_request_headers(headers, :delete, self.site.merge(path))) }
end
# Executes a PATCH request (see HTTP protocol documentation if unfamiliar).
# Used to update resources.
def patch(path, body = '', headers = {})
with_auth { request(:patch, path, body.to_s, build_request_headers(headers, :patch, self.site.merge(path))) }
end
# Executes a PUT request (see HTTP protocol documentation if unfamiliar).
# Used to update resources.
def put(path, body = '', headers = {})
with_auth { request(:put, path, body.to_s, build_request_headers(headers, :put, self.site.merge(path))) }
end
# Executes a POST request.
# Used to create new resources.
def post(path, body = '', headers = {})
with_auth { request(:post, path, body.to_s, build_request_headers(headers, :post, self.site.merge(path))) }
end
# Executes a HEAD request.
# Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
def head(path, headers = {})
with_auth { request(:head, path, build_request_headers(headers, :head, self.site.merge(path))) }
end
private
# Makes a request to the remote service.
def request(method, path, *arguments)
result = ActiveSupport::Notifications.instrument("request.active_resource") do |payload|
payload[:method] = method
payload[:request_uri] = "#{site.scheme}://#{site.host}:#{site.port}#{path}"
payload[:result] = http.send(method, path, *arguments)
end
handle_response(result)
rescue Timeout::Error => e
raise TimeoutError.new(e.message)
rescue OpenSSL::SSL::SSLError => e
raise SSLError.new(e.message)
end
# Handles response and error codes from the remote service.
def handle_response(response)
case response.code.to_i
when 301, 302, 303, 307
raise(Redirection.new(response))
when 200...400
response
when 400
raise(BadRequest.new(response))
when 401
raise(UnauthorizedAccess.new(response))
when 403
raise(ForbiddenAccess.new(response))
when 404
raise(ResourceNotFound.new(response))
when 405
raise(MethodNotAllowed.new(response))
when 409
raise(ResourceConflict.new(response))
when 410
raise(ResourceGone.new(response))
when 422
raise(ResourceInvalid.new(response))
when 401...500
raise(ClientError.new(response))
when 500...600
raise(ServerError.new(response))
else
raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
end
end
# Creates new Net::HTTP instance for communication with the
# remote service and resources.
def http
configure_http(new_http)
end
def new_http
if @proxy
Net::HTTP.new(@site.host, @site.port, @proxy.host, @proxy.port, @proxy.user, @proxy.password)
else
Net::HTTP.new(@site.host, @site.port)
end
end
def configure_http(http)
apply_ssl_options(http).tap do |https|
# Net::HTTP timeouts default to 60 seconds.
if defined? @timeout
https.open_timeout = @timeout
https.read_timeout = @timeout
end
end
end
def apply_ssl_options(http)
http.tap do |https|
# Skip config if site is already a https:// URI.
if defined? @ssl_options
http.use_ssl = true
# Default to no cert verification (WTF? FIXME)
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
# All the SSL options have corresponding http settings.
@ssl_options.each { |key, value| http.send "#{key}=", value }
end
end
end
def default_header
@default_header ||= {}
end
# Builds headers for request to remote service.
def build_request_headers(headers, http_method, uri)
authorization_header(http_method, uri).update(default_header).update(http_format_header(http_method)).update(headers)
end
def response_auth_header
@response_auth_header ||= ""
end
def with_auth
retried ||= false
yield
rescue UnauthorizedAccess => e
raise if retried || auth_type != :digest
@response_auth_header = e.response['WWW-Authenticate']
retried = true
retry
end
def authorization_header(http_method, uri)
if @user || @password
if auth_type == :digest
{ 'Authorization' => digest_auth_header(http_method, uri) }
else
{ 'Authorization' => 'Basic ' + ["#{@user}:#{@password}"].pack('m').delete("\r\n") }
end
else
{}
end
end
def digest_auth_header(http_method, uri)
params = extract_params_from_response
request_uri = uri.path
request_uri << "?#{uri.query}" if uri.query
ha1 = Digest::MD5.hexdigest("#{@user}:#{params['realm']}:#{@password}")
ha2 = Digest::MD5.hexdigest("#{http_method.to_s.upcase}:#{request_uri}")
params.merge!('cnonce' => client_nonce)
request_digest = Digest::MD5.hexdigest([ha1, params['nonce'], "0", params['cnonce'], params['qop'], ha2].join(":"))
"Digest #{auth_attributes_for(uri, request_digest, params)}"
end
def client_nonce
Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535)))
end
def extract_params_from_response
params = {}
if response_auth_header =~ /^(\w+) (.*)/
$2.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
end
params
end
def auth_attributes_for(uri, request_digest, params)
[
%Q(username="#{@user}"),
%Q(realm="#{params['realm']}"),
%Q(qop="#{params['qop']}"),
%Q(uri="#{uri.path}"),
%Q(nonce="#{params['nonce']}"),
%Q(nc="0"),
%Q(cnonce="#{params['cnonce']}"),
%Q(opaque="#{params['opaque']}"),
%Q(response="#{request_digest}")].join(", ")
end
def http_format_header(http_method)
{HTTP_FORMAT_HEADER_NAMES[http_method] => format.mime_type}
end
def legitimize_auth_type(auth_type)
return :basic if auth_type.nil?
auth_type = auth_type.to_sym
auth_type.in?([:basic, :digest]) ? auth_type : :basic
end
end
end
require 'active_support/core_ext/object/blank'
module ActiveResource
# A module to support custom REST methods and sub-resources, allowing you to break out
# of the "default" REST methods with your own custom resource requests. For example,
# say you use Rails to expose a REST service and configure your routes with:
#
# map.resources :people, :new => { :register => :post },
# :member => { :promote => :put, :deactivate => :delete }
# :collection => { :active => :get }
#
# This route set creates routes for the following HTTP requests:
#
# POST /people/new/register.json # PeopleController.register
# PATCH/PUT /people/1/promote.json # PeopleController.promote with :id => 1
# DELETE /people/1/deactivate.json # PeopleController.deactivate with :id => 1
# GET /people/active.json # PeopleController.active
#
# Using this module, Active Resource can use these custom REST methods just like the
# standard methods.
#
# class Person < ActiveResource::Base
# self.site = "https://37s.sunrise.com"
# end
#
# Person.new(:name => 'Ryan').post(:register) # POST /people/new/register.json
# # => { :id => 1, :name => 'Ryan' }
#
# Person.find(1).put(:promote, :position => 'Manager') # PUT /people/1/promote.json
# Person.find(1).delete(:deactivate) # DELETE /people/1/deactivate.json
#
# Person.get(:active) # GET /people/active.json
# # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
#
module CustomMethods
extend ActiveSupport::Concern
included do
class << self
alias :orig_delete :delete
# Invokes a GET to a given custom REST method. For example:
#
# Person.get(:active) # GET /people/active.json
# # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
#
# Person.get(:active, :awesome => true) # GET /people/active.json?awesome=true
# # => [{:id => 1, :name => 'Ryan'}]
#
# Note: the objects returned from this method are not automatically converted
# into ActiveResource::Base instances - they are ordinary Hashes. If you are expecting
# ActiveResource::Base instances, use the <tt>find</tt> class method with the
# <tt>:from</tt> option. For example:
#
# Person.find(:all, :from => :active)
def get(custom_method_name, options = {})
hashified = format.decode(connection.get(custom_method_collection_url(custom_method_name, options), headers).body)
derooted = Formats.remove_root(hashified)
derooted.is_a?(Array) ? derooted.map { |e| Formats.remove_root(e) } : derooted
end
def post(custom_method_name, options = {}, body = '')
connection.post(custom_method_collection_url(custom_method_name, options), body, headers)
end
def patch(custom_method_name, options = {}, body = '')
connection.patch(custom_method_collection_url(custom_method_name, options), body, headers)
end
def put(custom_method_name, options = {}, body = '')
connection.put(custom_method_collection_url(custom_method_name, options), body, headers)
end
def delete(custom_method_name, options = {})
# Need to jump through some hoops to retain the original class 'delete' method
if custom_method_name.is_a?(Symbol)
connection.delete(custom_method_collection_url(custom_method_name, options), headers)
else
orig_delete(custom_method_name, options)
end
end
end
end
module ClassMethods
def custom_method_collection_url(method_name, options = {})
prefix_options, query_options = split_options(options)
"#{prefix(prefix_options)}#{collection_name}/#{method_name}.#{format.extension}#{query_string(query_options)}"
end
end
def get(method_name, options = {})
self.class.format.decode(connection.get(custom_method_element_url(method_name, options), self.class.headers).body)
end
def post(method_name, options = {}, body = nil)
request_body = body.blank? ? encode : body
if new?
connection.post(custom_method_new_element_url(method_name, options), request_body, self.class.headers)
else
connection.post(custom_method_element_url(method_name, options), request_body, self.class.headers)
end
end
def patch(method_name, options = {}, body = '')
connection.patch(custom_method_element_url(method_name, options), body, self.class.headers)
end
def put(method_name, options = {}, body = '')
connection.put(custom_method_element_url(method_name, options), body, self.class.headers)
end
def delete(method_name, options = {})
connection.delete(custom_method_element_url(method_name, options), self.class.headers)
end
private
def custom_method_element_url(method_name, options = {})
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{id}/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}"
end
def custom_method_new_element_url(method_name, options = {})
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/new/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}"
end
end
end
module ActiveResource
class ConnectionError < StandardError # :nodoc:
attr_reader :response
def initialize(response, message = nil)
@response = response
@message = message
end
def to_s
message = "Failed."
message << " Response code = #{response.code}." if response.respond_to?(:code)
message << " Response message = #{response.message}." if response.respond_to?(:message)
message
end
end
# Raised when a Timeout::Error occurs.
class TimeoutError < ConnectionError
def initialize(message)
@message = message
end
def to_s; @message ;end
end
# Raised when a OpenSSL::SSL::SSLError occurs.
class SSLError < ConnectionError
def initialize(message)
@message = message
end
def to_s; @message ;end
end
# 3xx Redirection
class Redirection < ConnectionError # :nodoc:
def to_s
response['Location'] ? "#{super} => #{response['Location']}" : super
end
end
class MissingPrefixParam < ArgumentError # :nodoc:
end
# 4xx Client Error
class ClientError < ConnectionError # :nodoc:
end
# 400 Bad Request
class BadRequest < ClientError # :nodoc:
end
# 401 Unauthorized
class UnauthorizedAccess < ClientError # :nodoc:
end
# 403 Forbidden
class ForbiddenAccess < ClientError # :nodoc:
end
# 404 Not Found
class ResourceNotFound < ClientError # :nodoc:
end
# 409 Conflict
class ResourceConflict < ClientError # :nodoc:
end
# 410 Gone
class ResourceGone < ClientError # :nodoc:
end
# 5xx Server Error
class ServerError < ConnectionError # :nodoc:
end
# 405 Method Not Allowed
class MethodNotAllowed < ClientError # :nodoc:
def allowed_methods
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
end
end
end
module ActiveResource
module Formats
autoload :XmlFormat, 'active_resource/formats/xml_format'
autoload :JsonFormat, 'active_resource/formats/json_format'
# Lookup the format class from a mime type reference symbol. Example:
#
# ActiveResource::Formats[:xml] # => ActiveResource::Formats::XmlFormat
# ActiveResource::Formats[:json] # => ActiveResource::Formats::JsonFormat
def self.[](mime_type_reference)
ActiveResource::Formats.const_get(ActiveSupport::Inflector.camelize(mime_type_reference.to_s) + "Format")
end
def self.remove_root(data)
if data.is_a?(Hash) && data.keys.size == 1
data.values.first
else
data
end
end
end
end
require 'active_support/json'
module ActiveResource
module Formats
module JsonFormat
extend self
def extension
"json"
end
def mime_type
"application/json"
end
def encode(hash, options = nil)
ActiveSupport::JSON.encode(hash, options)
end
def decode(json)
Formats.remove_root(ActiveSupport::JSON.decode(json))
end
end
end
end
require 'active_support/core_ext/hash/conversions'
module ActiveResource
module Formats
module XmlFormat
extend self
def extension
"xml"
end
def mime_type
"application/xml"
end
def encode(hash, options={})
hash.to_xml(options)
end
def decode(xml)
Formats.remove_root(Hash.from_xml(xml))
end
end
end
end
require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/object/inclusion'
module ActiveResource
class InvalidRequestError < StandardError; end #:nodoc:
# One thing that has always been a pain with remote web services is testing. The HttpMock
# class makes it easy to test your Active Resource models by creating a set of mock responses to specific
# requests.
#
# To test your Active Resource model, you simply call the ActiveResource::HttpMock.respond_to
# method with an attached block. The block declares a set of URIs with expected input, and the output
# each request should return. The passed in block has any number of entries in the following generalized
# format:
#
# mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
#
# * <tt>http_method</tt> - The HTTP method to listen for. This can be +get+, +post+, +patch+, +put+, +delete+ or
# +head+.
# * <tt>path</tt> - A string, starting with a "/", defining the URI that is expected to be
# called.
# * <tt>request_headers</tt> - Headers that are expected along with the request. This argument uses a
# hash format, such as <tt>{ "Content-Type" => "application/json" }</tt>. This mock will only trigger
# if your tests sends a request with identical headers.
# * <tt>body</tt> - The data to be returned. This should be a string of Active Resource parseable content,
# such as Json.
# * <tt>status</tt> - The HTTP response code, as an integer, to return with the response.
# * <tt>response_headers</tt> - Headers to be returned with the response. Uses the same hash format as
# <tt>request_headers</tt> listed above.
#
# In order for a mock to deliver its content, the incoming request must match by the <tt>http_method</tt>,
# +path+ and <tt>request_headers</tt>. If no match is found an +InvalidRequestError+ exception
# will be raised showing you what request it could not find a response for and also what requests and response
# pairs have been recorded so you can create a new mock for that request.
#
# ==== Example
# def setup
# @matz = { :person => { :id => 1, :name => "Matz" } }.to_json
# ActiveResource::HttpMock.respond_to do |mock|
# mock.post "/people.json", {}, @matz, 201, "Location" => "/people/1.json"
# mock.get "/people/1.json", {}, @matz
# mock.put "/people/1.json", {}, nil, 204
# mock.delete "/people/1.json", {}, nil, 200
# end
# end
#
# def test_get_matz
# person = Person.find(1)
# assert_equal "Matz", person.name
# end
#
class HttpMock
class Responder #:nodoc:
def initialize(responses)
@responses = responses
end
[ :post, :patch, :put, :get, :delete, :head ].each do |method|
# def post(path, request_headers = {}, body = nil, status = 200, response_headers = {})
# @responses[Request.new(:post, path, nil, request_headers)] = Response.new(body || "", status, response_headers)
# end
module_eval <<-EOE, __FILE__, __LINE__ + 1
def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
request = Request.new(:#{method}, path, nil, request_headers)
response = Response.new(body || "", status, response_headers)
delete_duplicate_responses(request)
@responses << [request, response]
end
EOE
end
private
def delete_duplicate_responses(request)
@responses.delete_if {|r| r[0] == request }
end
end
class << self
# Returns an array of all request objects that have been sent to the mock. You can use this to check
# if your model actually sent an HTTP request.
#
# ==== Example
# def setup
# @matz = { :person => { :id => 1, :name => "Matz" } }.to_json
# ActiveResource::HttpMock.respond_to do |mock|
# mock.get "/people/1.json", {}, @matz
# end
# end
#
# def test_should_request_remote_service
# person = Person.find(1) # Call the remote service
#
# # This request object has the same HTTP method and path as declared by the mock
# expected_request = ActiveResource::Request.new(:get, "/people/1.json")
#
# # Assert that the mock received, and responded to, the expected request from the model
# assert ActiveResource::HttpMock.requests.include?(expected_request)
# end
def requests
@@requests ||= []
end
# Returns the list of requests and their mocked responses. Look up a
# response for a request using <tt>responses.assoc(request)</tt>.
def responses
@@responses ||= []
end
# Accepts a block which declares a set of requests and responses for the HttpMock to respond to in
# the following format:
#
# mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
#
# === Example
#
# @matz = { :person => { :id => 1, :name => "Matz" } }.to_json
# ActiveResource::HttpMock.respond_to do |mock|
# mock.post "/people.json", {}, @matz, 201, "Location" => "/people/1.json"
# mock.get "/people/1.json", {}, @matz
# mock.put "/people/1.json", {}, nil, 204
# mock.delete "/people/1.json", {}, nil, 200
# end
#
# Alternatively, accepts a hash of <tt>{Request => Response}</tt> pairs allowing you to generate
# these the following format:
#
# ActiveResource::Request.new(method, path, body, request_headers)
# ActiveResource::Response.new(body, status, response_headers)
#
# === Example
#
# Request.new(:#{method}, path, nil, request_headers)
#
# @matz = { :person => { :id => 1, :name => "Matz" } }.to_json
#
# create_matz = ActiveResource::Request.new(:post, '/people.json', @matz, {})
# created_response = ActiveResource::Response.new("", 201, {"Location" => "/people/1.json"})
# get_matz = ActiveResource::Request.new(:get, '/people/1.json', nil)
# ok_response = ActiveResource::Response.new("", 200, {})
#
# pairs = {create_matz => created_response, get_matz => ok_response}
#
# ActiveResource::HttpMock.respond_to(pairs)
#
# Note, by default, every time you call +respond_to+, any previous request and response pairs stored
# in HttpMock will be deleted giving you a clean slate to work on.
#
# If you want to override this behavior, pass in +false+ as the last argument to +respond_to+
#
# === Example
#
# ActiveResource::HttpMock.respond_to do |mock|
# mock.send(:get, "/people/1", {}, "JSON1")
# end
# ActiveResource::HttpMock.responses.length #=> 1
#
# ActiveResource::HttpMock.respond_to(false) do |mock|
# mock.send(:get, "/people/2", {}, "JSON2")
# end
# ActiveResource::HttpMock.responses.length #=> 2
#
# This also works with passing in generated pairs of requests and responses, again, just pass in false
# as the last argument:
#
# === Example
#
# ActiveResource::HttpMock.respond_to do |mock|
# mock.send(:get, "/people/1", {}, "JSON1")
# end
# ActiveResource::HttpMock.responses.length #=> 1
#
# get_matz = ActiveResource::Request.new(:get, '/people/1.json', nil)
# ok_response = ActiveResource::Response.new("", 200, {})
#
# pairs = {get_matz => ok_response}
#
# ActiveResource::HttpMock.respond_to(pairs, false)
# ActiveResource::HttpMock.responses.length #=> 2
#
# # If you add a response with an existing request, it will be replaced
#
# fail_response = ActiveResource::Response.new("", 404, {})
# pairs = {get_matz => fail_response}
#
# ActiveResource::HttpMock.respond_to(pairs, false)
# ActiveResource::HttpMock.responses.length #=> 2
#
def respond_to(*args) #:yields: mock
pairs = args.first || {}
reset! if args.last.class != FalseClass
if block_given?
yield Responder.new(responses)
else
delete_responses_to_replace pairs.to_a
responses.concat pairs.to_a
Responder.new(responses)
end
end
def delete_responses_to_replace(new_responses)
new_responses.each{|nr|
request_to_remove = nr[0]
@@responses = responses.delete_if{|r| r[0] == request_to_remove}
}
end
# Deletes all logged requests and responses.
def reset!
requests.clear
responses.clear
end
end
# body? methods
{ true => %w(post patch put),
false => %w(get delete head) }.each do |has_body, methods|
methods.each do |method|
# def post(path, body, headers)
# request = ActiveResource::Request.new(:post, path, body, headers)
# self.class.requests << request
# if response = self.class.responses.assoc(request)
# response[1]
# else
# raise InvalidRequestError.new("Could not find a response recorded for #{request.to_s} - Responses recorded are: - #{inspect_responses}")
# end
# end
module_eval <<-EOE, __FILE__, __LINE__ + 1
def #{method}(path, #{'body, ' if has_body}headers)
request = ActiveResource::Request.new(:#{method}, path, #{has_body ? 'body, ' : 'nil, '}headers)
self.class.requests << request
if response = self.class.responses.assoc(request)
response[1]
else
raise InvalidRequestError.new("Could not find a response recorded for \#{request.to_s} - Responses recorded are: \#{inspect_responses}")
end
end
EOE
end
end
def initialize(site) #:nodoc:
@site = site
end
def inspect_responses #:nodoc:
self.class.responses.map { |r| r[0].to_s }.inspect
end
end
class Request
attr_accessor :path, :method, :body, :headers
def initialize(method, path, body = nil, headers = {})
@method, @path, @body, @headers = method, path, body, headers
end
def ==(req)
path == req.path && method == req.method && headers_match?(req)
end
def to_s
"<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>"
end
private
def headers_match?(req)
# Ignore format header on equality if it's not defined
format_header = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[method]
if headers[format_header].present? || req.headers[format_header].blank?
headers == req.headers
else
headers.dup.merge(format_header => req.headers[format_header]) == req.headers
end
end
end
class Response
attr_accessor :body, :message, :code, :headers
def initialize(body, message = 200, headers = {})
@body, @message, @headers = body, message.to_s, headers
@code = @message[0,3].to_i
resp_cls = Net::HTTPResponse::CODE_TO_OBJ[@code.to_s]
if resp_cls && !resp_cls.body_permitted?
@body = nil
end
self['Content-Length'] = @body.nil? ? "0" : body.size.to_s
end
# Returns true if code is 2xx,
# false otherwise.
def success?
code.in?(200..299)
end
def [](key)
headers[key]
end
def []=(key, value)
headers[key] = value
end
# Returns true if the other is a Response with an equal body, equal message
# and equal headers. Otherwise it returns false.
def ==(other)
if (other.is_a?(Response))
other.body == body && other.message == message && other.headers == headers
else
false
end
end
end
class Connection
private
silence_warnings do
def http
@http ||= HttpMock.new(@site)
end
end
end
end
module ActiveResource
class LogSubscriber < ActiveSupport::LogSubscriber
def request(event)
result = event.payload[:result]
info "#{event.payload[:method].to_s.upcase} #{event.payload[:request_uri]}"
info "--> %d %s %d (%.1fms)" % [result.code, result.message, result.body.to_s.length, event.duration]
end
def logger
ActiveResource::Base.logger
end
end
end
ActiveResource::LogSubscriber.attach_to :active_resource
\ No newline at end of file
module ActiveResource
module Observing
extend ActiveSupport::Concern
include ActiveModel::Observing
included do
%w( create save update destroy ).each do |method|
# def create_with_notifications(*args, &block)
# notify_observers(:before_create)
# if result = create_without_notifications(*args, &block)
# notify_observers(:after_create)
# end
# result
# end
# alias_method_chain(create, :notifications)
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{method}_with_notifications(*args, &block)
notify_observers(:before_#{method})
if result = #{method}_without_notifications(*args, &block)
notify_observers(:after_#{method})
end
result
end
EOS
alias_method_chain(method, :notifications)
end
end
end
end
require "active_resource"
require "rails"
module ActiveResource
class Railtie < Rails::Railtie
config.active_resource = ActiveSupport::OrderedOptions.new
initializer "active_resource.set_configs" do |app|
app.config.active_resource.each do |k,v|
ActiveResource::Base.send "#{k}=", v
end
end
end
end
\ No newline at end of file
require 'active_resource/exceptions'
module ActiveResource # :nodoc:
class Schema # :nodoc:
# attributes can be known to be one of these types. They are easy to
# cast to/from.
KNOWN_ATTRIBUTE_TYPES = %w( string text integer float decimal datetime timestamp time date binary boolean )
# An array of attribute definitions, representing the attributes that
# have been defined.
attr_accessor :attrs
# The internals of an Active Resource Schema are very simple -
# unlike an Active Record TableDefinition (on which it is based).
# It provides a set of convenience methods for people to define their
# schema using the syntax:
# schema do
# string :foo
# integer :bar
# end
#
# The schema stores the name and type of each attribute. That is then
# read out by the schema method to populate the schema of the actual
# resource.
def initialize
@attrs = {}
end
def attribute(name, type, options = {})
raise ArgumentError, "Unknown Attribute type: #{type.inspect} for key: #{name.inspect}" unless type.nil? || Schema::KNOWN_ATTRIBUTE_TYPES.include?(type.to_s)
the_type = type.to_s
# TODO: add defaults
#the_attr = [type.to_s]
#the_attr << options[:default] if options.has_key? :default
@attrs[name.to_s] = the_type
self
end
# The following are the attribute types supported by Active Resource
# migrations.
KNOWN_ATTRIBUTE_TYPES.each do |attr_type|
# def string(*args)
# options = args.extract_options!
# attr_names = args
#
# attr_names.each { |name| attribute(name, 'string', options) }
# end
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{attr_type.to_s}(*args)
options = args.extract_options!
attr_names = args
attr_names.each { |name| attribute(name, '#{attr_type}', options) }
end
EOV
end
end
end
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/object/blank'
module ActiveResource
class ResourceInvalid < ClientError #:nodoc:
end
# Active Resource validation is reported to and from this object, which is used by Base#save
# to determine whether the object in a valid state to be saved. See usage example in Validations.
class Errors < ActiveModel::Errors
# Grabs errors from an array of messages (like ActiveRecord::Validations).
# The second parameter directs the errors cache to be cleared (default)
# or not (by passing true).
def from_array(messages, save_cache = false)
clear unless save_cache
humanized_attributes = Hash[@base.attributes.keys.map { |attr_name| [attr_name.humanize, attr_name] }]
messages.each do |message|
attr_message = humanized_attributes.keys.detect do |attr_name|
if message[0, attr_name.size + 1] == "#{attr_name} "
add humanized_attributes[attr_name], message[(attr_name.size + 1)..-1]
end
end
self[:base] << message if attr_message.nil?
end
end
# Grabs errors from a hash of attribute => array of errors elements
# The second parameter directs the errors cache to be cleared (default)
# or not (by passing true)
#
# Unrecognized attribute names will be humanized and added to the record's
# base errors.
def from_hash(messages, save_cache = false)
clear unless save_cache
messages.each do |(key,errors)|
errors.each do |error|
if @base.attributes.keys.include?(key)
add key, error
elsif key == 'base'
self[:base] << error
else
# reporting an error on an attribute not in attributes
# format and add them to base
self[:base] << "#{key.humanize} #{error}"
end
end
end
end
# Grabs errors from a json response.
def from_json(json, save_cache = false)
decoded = ActiveSupport::JSON.decode(json) || {} rescue {}
if decoded.kind_of?(Hash) && (decoded.has_key?('errors') || decoded.empty?)
errors = decoded['errors'] || {}
if errors.kind_of?(Array)
# 3.2.1-style with array of strings
ActiveSupport::Deprecation.warn('Returning errors as an array of strings is deprecated.')
from_array errors, save_cache
else
# 3.2.2+ style
from_hash errors, save_cache
end
else
# <3.2-style respond_with - lacks 'errors' key
ActiveSupport::Deprecation.warn('Returning errors as a hash without a root "errors" key is deprecated.')
from_hash decoded, save_cache
end
end
# Grabs errors from an XML response.
def from_xml(xml, save_cache = false)
array = Array.wrap(Hash.from_xml(xml)['errors']['error']) rescue []
from_array array, save_cache
end
end
# Module to support validation and errors with Active Resource objects. The module overrides
# Base#save to rescue ActiveResource::ResourceInvalid exceptions and parse the errors returned
# in the web service response. The module also adds an +errors+ collection that mimics the interface
# of the errors provided by ActiveModel::Errors.
#
# ==== Example
#
# Consider a Person resource on the server requiring both a +first_name+ and a +last_name+ with a
# <tt>validates_presence_of :first_name, :last_name</tt> declaration in the model:
#
# person = Person.new(:first_name => "Jim", :last_name => "")
# person.save # => false (server returns an HTTP 422 status code and errors)
# person.valid? # => false
# person.errors.empty? # => false
# person.errors.count # => 1
# person.errors.full_messages # => ["Last name can't be empty"]
# person.errors[:last_name] # => ["can't be empty"]
# person.last_name = "Halpert"
# person.save # => true (and person is now saved to the remote service)
#
module Validations
extend ActiveSupport::Concern
include ActiveModel::Validations
included do
alias_method_chain :save, :validation
end
# Validate a resource and save (POST) it to the remote web service.
# If any local validations fail - the save (POST) will not be attempted.
def save_with_validation(options={})
perform_validation = options[:validate] != false
# clear the remote validations so they don't interfere with the local
# ones. Otherwise we get an endless loop and can never change the
# fields so as to make the resource valid.
@remote_errors = nil
if perform_validation && valid? || !perform_validation
save_without_validation
true
else
false
end
rescue ResourceInvalid => error
# cache the remote errors because every call to <tt>valid?</tt> clears
# all errors. We must keep a copy to add these back after local
# validations.
@remote_errors = error
load_remote_errors(@remote_errors, true)
false
end
# Loads the set of remote errors into the object's Errors based on the
# content-type of the error-block received.
def load_remote_errors(remote_errors, save_cache = false ) #:nodoc:
case self.class.format
when ActiveResource::Formats[:xml]
errors.from_xml(remote_errors.response.body, save_cache)
when ActiveResource::Formats[:json]
errors.from_json(remote_errors.response.body, save_cache)
end
end
# Checks for errors on an object (i.e., is resource.errors empty?).
#
# Runs all the specified local validations and returns true if no errors
# were added, otherwise false.
# Runs local validations (eg those on your Active Resource model), and
# also any errors returned from the remote system the last time we
# saved.
# Remote errors can only be cleared by trying to re-save the resource.
#
# ==== Examples
# my_person = Person.create(params[:person])
# my_person.valid?
# # => true
#
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
# my_person.valid?
# # => false
#
def valid?
super
load_remote_errors(@remote_errors, true) if defined?(@remote_errors) && @remote_errors.present?
errors.empty?
end
# Returns the Errors object that holds all information about attribute error messages.
def errors
@errors ||= Errors.new(self)
end
end
end
module ActiveResource
module VERSION #:nodoc:
MAJOR = 4
MINOR = 0
TINY = 0
PRE = "beta"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
end
end
require File.expand_path('../../../load_paths', __FILE__)
lib = File.expand_path("#{File.dirname(__FILE__)}/../lib")
$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib)
require 'minitest/autorun'
require 'active_resource'
require 'active_support'
require 'active_support/test_case'
require 'setter_trap'
require 'active_support/logger'
ActiveResource::Base.logger = ActiveSupport::Logger.new("#{File.dirname(__FILE__)}/debug.log")
def setup_response
matz_hash = { 'person' => { :id => 1, :name => 'Matz' } }
@default_request_headers = { 'Content-Type' => 'application/json' }
@matz = matz_hash.to_json
@matz_xml = matz_hash.to_xml
@david = { :person => { :id => 2, :name => 'David' } }.to_json
@greg = { :person => { :id => 3, :name => 'Greg' } }.to_json
@addy = { :address => { :id => 1, :street => '12345 Street', :country => 'Australia' } }.to_json
@rick = { :person => { :name => "Rick", :age => 25 } }.to_json
@joe = { :person => { :id => 6, :name => 'Joe', :likes_hats => true }}.to_json
@people = { :people => [ { :person => { :id => 1, :name => 'Matz' } }, { :person => { :id => 2, :name => 'David' } }] }.to_json
@people_david = { :people => [ { :person => { :id => 2, :name => 'David' } }] }.to_json
@addresses = { :addresses => [{ :address => { :id => 1, :street => '12345 Street', :country => 'Australia' } }] }.to_json
# - deep nested resource -
# - Luis (Customer)
# - JK (Customer::Friend)
# - Mateo (Customer::Friend::Brother)
# - Edith (Customer::Friend::Brother::Child)
# - Martha (Customer::Friend::Brother::Child)
# - Felipe (Customer::Friend::Brother)
# - Bryan (Customer::Friend::Brother::Child)
# - Luke (Customer::Friend::Brother::Child)
# - Eduardo (Customer::Friend)
# - Sebas (Customer::Friend::Brother)
# - Andres (Customer::Friend::Brother::Child)
# - Jorge (Customer::Friend::Brother::Child)
# - Elsa (Customer::Friend::Brother)
# - Natacha (Customer::Friend::Brother::Child)
# - Milena (Customer::Friend::Brother)
#
@luis = {
:customer => {
:id => 1,
:name => 'Luis',
:friends => [{
:name => 'JK',
:brothers => [
{
:name => 'Mateo',
:children => [{ :name => 'Edith' },{ :name => 'Martha' }]
}, {
:name => 'Felipe',
:children => [{ :name => 'Bryan' },{ :name => 'Luke' }]
}
]
}, {
:name => 'Eduardo',
:brothers => [
{
:name => 'Sebas',
:children => [{ :name => 'Andres' },{ :name => 'Jorge' }]
}, {
:name => 'Elsa',
:children => [{ :name => 'Natacha' }]
}, {
:name => 'Milena',
:children => []
}
]
}]
}
}.to_json
# - resource with yaml array of strings; for ARs using serialize :bar, Array
@marty = <<-eof.strip
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<person>
<id type=\"integer\">5</id>
<name>Marty</name>
<colors type=\"yaml\">---
- \"red\"
- \"green\"
- \"blue\"
</colors>
</person>
eof
@startup_sound = {
:sound => {
:name => "Mac Startup Sound", :author => { :name => "Jim Reekes" }
}
}.to_json
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/1.json", {}, @matz
mock.get "/people/1.xml", {}, @matz_xml
mock.get "/people/2.xml", {}, @david
mock.get "/people/5.xml", {}, @marty
mock.get "/people/Greg.json", {}, @greg
mock.get "/people/6.json", {}, @joe
mock.get "/people/4.json", { 'key' => 'value' }, nil, 404
mock.put "/people/1.json", {}, nil, 204
mock.delete "/people/1.json", {}, nil, 200
mock.delete "/people/2.xml", {}, nil, 400
mock.get "/people/99.json", {}, nil, 404
mock.post "/people.json", {}, @rick, 201, 'Location' => '/people/5.xml'
mock.get "/people.json", {}, @people
mock.get "/people/1/addresses.json", {}, @addresses
mock.get "/people/1/addresses/1.json", {}, @addy
mock.get "/people/1/addresses/2.xml", {}, nil, 404
mock.get "/people/2/addresses.json", {}, nil, 404
mock.get "/people/2/addresses/1.xml", {}, nil, 404
mock.get "/people/Greg/addresses/1.json", {}, @addy
mock.put "/people/1/addresses/1.json", {}, nil, 204
mock.delete "/people/1/addresses/1.json", {}, nil, 200
mock.post "/people/1/addresses.json", {}, nil, 201, 'Location' => '/people/1/addresses/5'
mock.get "/people/1/addresses/99.json", {}, nil, 404
mock.get "/people//addresses.xml", {}, nil, 404
mock.get "/people//addresses/1.xml", {}, nil, 404
mock.put "/people//addresses/1.xml", {}, nil, 404
mock.delete "/people//addresses/1.xml", {}, nil, 404
mock.post "/people//addresses.xml", {}, nil, 404
mock.head "/people/1.json", {}, nil, 200
mock.head "/people/Greg.json", {}, nil, 200
mock.head "/people/99.json", {}, nil, 404
mock.head "/people/1/addresses/1.json", {}, nil, 200
mock.head "/people/1/addresses/2.json", {}, nil, 404
mock.head "/people/2/addresses/1.json", {}, nil, 404
mock.head "/people/Greg/addresses/1.json", {}, nil, 200
# customer
mock.get "/customers/1.json", {}, @luis
# sound
mock.get "/sounds/1.json", {}, @startup_sound
end
Person.user = nil
Person.password = nil
end
require 'abstract_unit'
class AuthorizationTest < ActiveSupport::TestCase
Response = Struct.new(:code)
def setup
@conn = ActiveResource::Connection.new('http://localhost')
@matz = { :person => { :id => 1, :name => 'Matz' } }.to_json
@david = { :person => { :id => 2, :name => 'David' } }.to_json
@authenticated_conn = ActiveResource::Connection.new("http://david:test123@localhost")
@basic_authorization_request_header = { 'Authorization' => 'Basic ZGF2aWQ6dGVzdDEyMw==' }
end
private
def decode(response)
@authenticated_conn.format.decode(response.body)
end
end
class BasicAuthorizationTest < AuthorizationTest
def setup
super
@authenticated_conn.auth_type = :basic
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/2.json", @basic_authorization_request_header, @david
mock.get "/people/1.json", @basic_authorization_request_header, nil, 401, { 'WWW-Authenticate' => 'i_should_be_ignored' }
mock.put "/people/2.json", @basic_authorization_request_header, nil, 204
mock.delete "/people/2.json", @basic_authorization_request_header, nil, 200
mock.post "/people/2/addresses.json", @basic_authorization_request_header, nil, 201, 'Location' => '/people/1/addresses/5'
mock.head "/people/2.json", @basic_authorization_request_header, nil, 200
end
end
def test_get
david = decode(@authenticated_conn.get("/people/2.json"))
assert_equal "David", david["name"]
end
def test_post
response = @authenticated_conn.post("/people/2/addresses.json")
assert_equal "/people/1/addresses/5", response["Location"]
end
def test_put
response = @authenticated_conn.put("/people/2.json")
assert_equal 204, response.code
end
def test_delete
response = @authenticated_conn.delete("/people/2.json")
assert_equal 200, response.code
end
def test_head
response = @authenticated_conn.head("/people/2.json")
assert_equal 200, response.code
end
def test_retry_on_401_doesnt_happen_with_basic_auth
assert_raise(ActiveResource::UnauthorizedAccess) { @authenticated_conn.get("/people/1.json") }
assert_equal "", @authenticated_conn.send(:response_auth_header)
end
def test_raises_invalid_request_on_unauthorized_requests
assert_raise(ActiveResource::InvalidRequestError) { @conn.get("/people/2.json") }
assert_raise(ActiveResource::InvalidRequestError) { @conn.post("/people/2/addresses.json") }
assert_raise(ActiveResource::InvalidRequestError) { @conn.put("/people/2.json") }
assert_raise(ActiveResource::InvalidRequestError) { @conn.delete("/people/2.json") }
assert_raise(ActiveResource::InvalidRequestError) { @conn.head("/people/2.json") }
end
def test_authorization_header
authorization_header = @authenticated_conn.__send__(:authorization_header, :get, URI.parse('/people/2.json'))
assert_equal @basic_authorization_request_header['Authorization'], authorization_header['Authorization']
authorization = authorization_header["Authorization"].to_s.split
assert_equal "Basic", authorization[0]
assert_equal ["david", "test123"], ::Base64.decode64(authorization[1]).split(":")[0..1]
end
def test_authorization_header_with_username_but_no_password
@conn = ActiveResource::Connection.new("http://david:@localhost")
authorization_header = @conn.__send__(:authorization_header, :get, URI.parse('/people/2.json'))
authorization = authorization_header["Authorization"].to_s.split
assert_equal "Basic", authorization[0]
assert_equal ["david"], ::Base64.decode64(authorization[1]).split(":")[0..1]
end
def test_authorization_header_with_password_but_no_username
@conn = ActiveResource::Connection.new("http://:test123@localhost")
authorization_header = @conn.__send__(:authorization_header, :get, URI.parse('/people/2.json'))
authorization = authorization_header["Authorization"].to_s.split
assert_equal "Basic", authorization[0]
assert_equal ["", "test123"], ::Base64.decode64(authorization[1]).split(":")[0..1]
end
def test_authorization_header_with_decoded_credentials_from_url
@conn = ActiveResource::Connection.new("http://my%40email.com:%31%32%33@localhost")
authorization_header = @conn.__send__(:authorization_header, :get, URI.parse('/people/2.json'))
authorization = authorization_header["Authorization"].to_s.split
assert_equal "Basic", authorization[0]
assert_equal ["my@email.com", "123"], ::Base64.decode64(authorization[1]).split(":")[0..1]
end
def test_authorization_header_explicitly_setting_username_and_password
@authenticated_conn = ActiveResource::Connection.new("http://@localhost")
@authenticated_conn.user = 'david'
@authenticated_conn.password = 'test123'
authorization_header = @authenticated_conn.__send__(:authorization_header, :get, URI.parse('/people/2.json'))
assert_equal @basic_authorization_request_header['Authorization'], authorization_header['Authorization']
authorization = authorization_header["Authorization"].to_s.split
assert_equal "Basic", authorization[0]
assert_equal ["david", "test123"], ::Base64.decode64(authorization[1]).split(":")[0..1]
end
def test_authorization_header_explicitly_setting_username_but_no_password
@conn = ActiveResource::Connection.new("http://@localhost")
@conn.user = "david"
authorization_header = @conn.__send__(:authorization_header, :get, URI.parse('/people/2.json'))
authorization = authorization_header["Authorization"].to_s.split
assert_equal "Basic", authorization[0]
assert_equal ["david"], ::Base64.decode64(authorization[1]).split(":")[0..1]
end
def test_authorization_header_explicitly_setting_password_but_no_username
@conn = ActiveResource::Connection.new("http://@localhost")
@conn.password = "test123"
authorization_header = @conn.__send__(:authorization_header, :get, URI.parse('/people/2.json'))
authorization = authorization_header["Authorization"].to_s.split
assert_equal "Basic", authorization[0]
assert_equal ["", "test123"], ::Base64.decode64(authorization[1]).split(":")[0..1]
end
def test_authorization_header_if_credentials_supplied_and_auth_type_is_basic
authorization_header = @authenticated_conn.__send__(:authorization_header, :get, URI.parse('/people/2.json'))
assert_equal @basic_authorization_request_header['Authorization'], authorization_header['Authorization']
authorization = authorization_header["Authorization"].to_s.split
assert_equal "Basic", authorization[0]
assert_equal ["david", "test123"], ::Base64.decode64(authorization[1]).split(":")[0..1]
end
def test_client_nonce_is_not_nil
assert_not_nil ActiveResource::Connection.new("http://david:test123@localhost").send(:client_nonce)
end
end
class DigestAuthorizationTest < AuthorizationTest
def setup
super
@authenticated_conn.auth_type = :digest
# Make client nonce deterministic
def @authenticated_conn.client_nonce; 'i-am-a-client-nonce' end
@nonce = "MTI0OTUxMzc4NzpjYWI3NDM3NDNmY2JmODU4ZjQ2ZjcwNGZkMTJiMjE0NA=="
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/2.json", { 'Authorization' => blank_digest_auth_header("/people/2.json", "fad396f6a34aeba28e28b9b96ddbb671") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header }
mock.get "/people/2.json", { 'Authorization' => request_digest_auth_header("/people/2.json", "c064d5ba8891a25290c76c8c7d31fb7b") }, @david, 200
mock.get "/people/1.json", { 'Authorization' => request_digest_auth_header("/people/1.json", "f9c0b594257bb8422af4abd429c5bb70") }, @matz, 200
mock.put "/people/2.json", { 'Authorization' => blank_digest_auth_header("/people/2.json", "50a685d814f94665b9d160fbbaa3958a") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header }
mock.put "/people/2.json", { 'Authorization' => request_digest_auth_header("/people/2.json", "5a75cde841122d8e0f20f8fd1f98a743") }, nil, 204
mock.delete "/people/2.json", { 'Authorization' => blank_digest_auth_header("/people/2.json", "846f799107eab5ca4285b909ee299a33") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header }
mock.delete "/people/2.json", { 'Authorization' => request_digest_auth_header("/people/2.json", "9f5b155224edbbb69fd99d8ce094681e") }, nil, 200
mock.post "/people/2/addresses.json", { 'Authorization' => blank_digest_auth_header("/people/2/addresses.json", "6984d405ff3d9ed07bbf747dcf16afb0") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header }
mock.post "/people/2/addresses.json", { 'Authorization' => request_digest_auth_header("/people/2/addresses.json", "4bda6a28dbf930b5af9244073623bd04") }, nil, 201, 'Location' => '/people/1/addresses/5'
mock.head "/people/2.json", { 'Authorization' => blank_digest_auth_header("/people/2.json", "15e5ed84ba5c4cfcd5c98a36c2e4f421") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header }
mock.head "/people/2.json", { 'Authorization' => request_digest_auth_header("/people/2.json", "d4c6d2bcc8717abb2e2ccb8c49ee6a91") }, nil, 200
end
end
def test_authorization_header_if_credentials_supplied_and_auth_type_is_digest
authorization_header = @authenticated_conn.__send__(:authorization_header, :get, URI.parse('/people/2.json'))
assert_equal blank_digest_auth_header("/people/2.json", "fad396f6a34aeba28e28b9b96ddbb671"), authorization_header['Authorization']
end
def test_authorization_header_with_query_string_if_auth_type_is_digest
authorization_header = @authenticated_conn.__send__(:authorization_header, :get, URI.parse('/people/2.json?only=name'))
assert_equal blank_digest_auth_header("/people/2.json?only=name", "f8457b0b5d21b6b80737a386217afb24"), authorization_header['Authorization']
end
def test_get_with_digest_auth_handles_initial_401_response_and_retries
response = @authenticated_conn.get("/people/2.json")
assert_equal "David", decode(response)["name"]
end
def test_post_with_digest_auth_handles_initial_401_response_and_retries
response = @authenticated_conn.post("/people/2/addresses.json")
assert_equal "/people/1/addresses/5", response["Location"]
assert_equal 201, response.code
end
def test_put_with_digest_auth_handles_initial_401_response_and_retries
response = @authenticated_conn.put("/people/2.json")
assert_equal 204, response.code
end
def test_delete_with_digest_auth_handles_initial_401_response_and_retries
response = @authenticated_conn.delete("/people/2.json")
assert_equal 200, response.code
end
def test_head_with_digest_auth_handles_initial_401_response_and_retries
response = @authenticated_conn.head("/people/2.json")
assert_equal 200, response.code
end
def test_get_with_digest_auth_caches_nonce
response = @authenticated_conn.get("/people/2.json")
assert_equal "David", decode(response)["name"]
# There is no mock for this request with a non-cached nonce.
response = @authenticated_conn.get("/people/1.json")
assert_equal "Matz", decode(response)["name"]
end
def test_raises_invalid_request_on_unauthorized_requests_with_digest_auth
@conn.auth_type = :digest
assert_raise(ActiveResource::InvalidRequestError) { @conn.get("/people/2.json") }
assert_raise(ActiveResource::InvalidRequestError) { @conn.post("/people/2/addresses.json") }
assert_raise(ActiveResource::InvalidRequestError) { @conn.put("/people/2.json") }
assert_raise(ActiveResource::InvalidRequestError) { @conn.delete("/people/2.json") }
assert_raise(ActiveResource::InvalidRequestError) { @conn.head("/people/2.json") }
end
private
def blank_digest_auth_header(uri, response)
%Q(Digest username="david", realm="", qop="", uri="#{uri}", nonce="", nc="0", cnonce="i-am-a-client-nonce", opaque="", response="#{response}")
end
def request_digest_auth_header(uri, response)
%Q(Digest username="david", realm="RailsTestApp", qop="auth", uri="#{uri}", nonce="#{@nonce}", nc="0", cnonce="i-am-a-client-nonce", opaque="ef6dfb078ba22298d366f99567814ffb", response="#{response}")
end
def response_digest_auth_header
%Q(Digest realm="RailsTestApp", qop="auth", algorithm=MD5, nonce="#{@nonce}", opaque="ef6dfb078ba22298d366f99567814ffb")
end
end
require 'abstract_unit'
require 'fixtures/person'
require 'fixtures/street_address'
require 'active_support/core_ext/hash/conversions'
class CustomMethodsTest < ActiveSupport::TestCase
def setup
@matz = { :person => { :id => 1, :name => 'Matz' } }.to_json
@matz_deep = { :person => { :id => 1, :name => 'Matz', :other => 'other' } }.to_json
@matz_array = { :people => [{ :person => { :id => 1, :name => 'Matz' } }] }.to_json
@ryan = { :person => { :name => 'Ryan' } }.to_json
@addy = { :address => { :id => 1, :street => '12345 Street' } }.to_json
@addy_deep = { :address => { :id => 1, :street => '12345 Street', :zip => "27519" } }.to_json
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/1.json", {}, @matz
mock.get "/people/1/shallow.json", {}, @matz
mock.get "/people/1/deep.json", {}, @matz_deep
mock.get "/people/retrieve.json?name=Matz", {}, @matz_array
mock.get "/people/managers.json", {}, @matz_array
mock.post "/people/hire.json?name=Matz", {}, nil, 201
mock.put "/people/1/promote.json?position=Manager", {}, nil, 204
mock.put "/people/promote.json?name=Matz", {}, nil, 204, {}
mock.put "/people/sort.json?by=name", {}, nil, 204
mock.delete "/people/deactivate.json?name=Matz", {}, nil, 200
mock.delete "/people/1/deactivate.json", {}, nil, 200
mock.post "/people/new/register.json", {}, @ryan, 201, 'Location' => '/people/5.json'
mock.post "/people/1/register.json", {}, @matz, 201
mock.get "/people/1/addresses/1.json", {}, @addy
mock.get "/people/1/addresses/1/deep.json", {}, @addy_deep
mock.put "/people/1/addresses/1/normalize_phone.json?locale=US", {}, nil, 204
mock.put "/people/1/addresses/sort.json?by=name", {}, nil, 204
mock.post "/people/1/addresses/new/link.json", {}, { :address => { :street => '12345 Street' } }.to_json, 201, 'Location' => '/people/1/addresses/2.json'
end
Person.user = nil
Person.password = nil
end
def teardown
ActiveResource::HttpMock.reset!
end
def test_custom_collection_method
# GET
assert_equal([{ "id" => 1, "name" => 'Matz' }], Person.get(:retrieve, :name => 'Matz'))
# POST
assert_equal(ActiveResource::Response.new("", 201, {}), Person.post(:hire, :name => 'Matz'))
# PUT
assert_equal ActiveResource::Response.new("", 204, {}),
Person.put(:promote, {:name => 'Matz'}, 'atestbody')
assert_equal ActiveResource::Response.new("", 204, {}), Person.put(:sort, :by => 'name')
# DELETE
Person.delete :deactivate, :name => 'Matz'
# Nested resource
assert_equal ActiveResource::Response.new("", 204, {}), StreetAddress.put(:sort, :person_id => 1, :by => 'name')
end
def test_custom_element_method
# Test GET against an element URL
assert_equal Person.find(1).get(:shallow), {"id" => 1, "name" => 'Matz'}
assert_equal Person.find(1).get(:deep), {"id" => 1, "name" => 'Matz', "other" => 'other'}
# Test PUT against an element URL
assert_equal ActiveResource::Response.new("", 204, {}), Person.find(1).put(:promote, {:position => 'Manager'}, 'body')
# Test DELETE against an element URL
assert_equal ActiveResource::Response.new("", 200, {}), Person.find(1).delete(:deactivate)
# With nested resources
assert_equal StreetAddress.find(1, :params => { :person_id => 1 }).get(:deep),
{ "id" => 1, "street" => '12345 Street', "zip" => "27519" }
assert_equal ActiveResource::Response.new("", 204, {}),
StreetAddress.find(1, :params => { :person_id => 1 }).put(:normalize_phone, :locale => 'US')
end
def test_custom_new_element_method
# Test POST against a new element URL
ryan = Person.new(:name => 'Ryan')
assert_equal ActiveResource::Response.new(@ryan, 201, { 'Location' => '/people/5.json' }), ryan.post(:register)
expected_request = ActiveResource::Request.new(:post, '/people/new/register.json', @ryan)
assert_equal expected_request.body, ActiveResource::HttpMock.requests.first.body
# Test POST against a nested collection URL
addy = StreetAddress.new(:street => '123 Test Dr.', :person_id => 1)
assert_equal ActiveResource::Response.new({ :address => { :street => '12345 Street' } }.to_json,
201, { 'Location' => '/people/1/addresses/2.json' }),
addy.post(:link)
matz = Person.find(1)
assert_equal ActiveResource::Response.new(@matz, 201), matz.post(:register)
end
def test_find_custom_resources
assert_equal 'Matz', Person.find(:all, :from => :managers).first.name
end
end
require 'abstract_unit'
require "fixtures/person"
require "fixtures/street_address"
class BaseEqualityTest < ActiveSupport::TestCase
def setup
@new = Person.new
@one = Person.new(:id => 1)
@two = Person.new(:id => 2)
@street = StreetAddress.new(:id => 2)
end
def test_should_equal_self
assert @new == @new, '@new == @new'
assert @one == @one, '@one == @one'
end
def test_shouldnt_equal_new_resource
assert @new != @one, '@new != @one'
assert @one != @new, '@one != @new'
end
def test_shouldnt_equal_different_class
assert @two != @street, 'person != street_address with same id'
assert @street != @two, 'street_address != person with same id'
end
def test_eql_should_alias_equals_operator
assert_equal @new == @new, @new.eql?(@new)
assert_equal @new == @one, @new.eql?(@one)
assert_equal @one == @one, @one.eql?(@one)
assert_equal @one == @new, @one.eql?(@new)
assert_equal @one == @street, @one.eql?(@street)
end
def test_hash_should_be_id_hash
[@new, @one, @two, @street].each do |resource|
assert_equal resource.id.hash, resource.hash
end
end
def test_with_prefix_options
assert_equal @one == @one, @one.eql?(@one)
assert_equal @one == @one.dup, @one.eql?(@one.dup)
new_one = @one.dup
new_one.prefix_options = {:foo => 'bar'}
assert_not_equal @one, new_one
end
end
require 'abstract_unit'
require "fixtures/person"
require "fixtures/street_address"
require 'active_support/core_ext/hash/conversions'
module Highrise
class Note < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000"
end
class Comment < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000"
end
module Deeply
module Nested
class Note < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000"
end
class Comment < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000"
end
module TestDifferentLevels
class Note < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000"
end
end
end
end
end
class BaseLoadTest < ActiveSupport::TestCase
def setup
@matz = { :id => 1, :name => 'Matz' }
@first_address = { :address => { :id => 1, :street => '12345 Street' } }
@addresses = [@first_address, { :address => { :id => 2, :street => '67890 Street' } }]
@addresses_from_json = { :street_addresses => @addresses }
@addresses_from_json_single = { :street_addresses => [ @first_address ] }
@deep = { :id => 1, :street => {
:id => 1, :state => { :id => 1, :name => 'Oregon',
:notable_rivers => [
{ :id => 1, :name => 'Willamette' },
{ :id => 2, :name => 'Columbia', :rafted_by => @matz }],
:postal_codes => [ 97018, 1234567890 ],
:dates => [ Time.now ],
:votes => [ true, false, true ],
:places => [ "Columbia City", "Unknown" ]}}}
# List of books formated as [{timestamp_of_publication => name}, ...]
@books = {:books => [
{1009839600 => "Ruby in a Nutshell"},
{1199142000 => "The Ruby Programming Language"}
]}
@books_date = {:books => [
{Time.at(1009839600) => "Ruby in a Nutshell"},
{Time.at(1199142000) => "The Ruby Programming Language"}
]}
@person = Person.new
end
def test_load_hash_with_integers_as_keys
assert_nothing_raised{@person.load(@books)}
end
def test_load_hash_with_dates_as_keys
assert_nothing_raised{@person.load(@books_date)}
end
def test_load_expects_hash
assert_raise(ArgumentError) { @person.load nil }
assert_raise(ArgumentError) { @person.load '<person id="1"/>' }
end
def test_load_simple_hash
assert_equal Hash.new, @person.attributes
assert_equal @matz.stringify_keys, @person.load(@matz).attributes
end
def test_after_load_attributes_are_accessible
assert_equal Hash.new, @person.attributes
assert_equal @matz.stringify_keys, @person.load(@matz).attributes
assert_equal @matz[:name], @person.attributes['name']
end
def test_after_load_attributes_are_accessible_via_indifferent_access
assert_equal Hash.new, @person.attributes
assert_equal @matz.stringify_keys, @person.load(@matz).attributes
assert_equal @matz[:name], @person.attributes['name']
assert_equal @matz[:name], @person.attributes[:name]
end
def test_load_one_with_existing_resource
address = @person.load(:street_address => @first_address.values.first).street_address
assert_kind_of StreetAddress, address
assert_equal @first_address.values.first.stringify_keys, address.attributes
end
def test_load_one_with_unknown_resource
address = silence_warnings { @person.load(@first_address).address }
assert_kind_of Person::Address, address
assert_equal @first_address.values.first.stringify_keys, address.attributes
end
def test_load_collection_with_existing_resource
addresses = @person.load(@addresses_from_json).street_addresses
assert_kind_of Array, addresses
addresses.each { |address| assert_kind_of StreetAddress, address }
assert_equal @addresses.map { |a| a[:address].stringify_keys }, addresses.map(&:attributes)
end
def test_load_collection_with_unknown_resource
Person.__send__(:remove_const, :Address) if Person.const_defined?(:Address)
assert !Person.const_defined?(:Address), "Address shouldn't exist until autocreated"
addresses = silence_warnings { @person.load(:addresses => @addresses).addresses }
assert Person.const_defined?(:Address), "Address should have been autocreated"
addresses.each { |address| assert_kind_of Person::Address, address }
assert_equal @addresses.map { |a| a[:address].stringify_keys }, addresses.map(&:attributes)
end
def test_load_collection_with_single_existing_resource
addresses = @person.load(@addresses_from_json_single).street_addresses
assert_kind_of Array, addresses
addresses.each { |address| assert_kind_of StreetAddress, address }
assert_equal [ @first_address.values.first ].map(&:stringify_keys), addresses.map(&:attributes)
end
def test_load_collection_with_single_unknown_resource
Person.__send__(:remove_const, :Address) if Person.const_defined?(:Address)
assert !Person.const_defined?(:Address), "Address shouldn't exist until autocreated"
addresses = silence_warnings { @person.load(:addresses => [ @first_address ]).addresses }
assert Person.const_defined?(:Address), "Address should have been autocreated"
addresses.each { |address| assert_kind_of Person::Address, address }
assert_equal [ @first_address.values.first ].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 Array, 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
postal_codes = state.postal_codes
assert_kind_of Array, postal_codes
assert_equal 2, postal_codes.size
assert_kind_of Fixnum, postal_codes.first
assert_equal @deep[:street][:state][:postal_codes].first, postal_codes.first
assert_kind_of Numeric, postal_codes.last
assert_equal @deep[:street][:state][:postal_codes].last, postal_codes.last
places = state.places
assert_kind_of Array, places
assert_kind_of String, places.first
assert_equal @deep[:street][:state][:places].first, places.first
dates = state.dates
assert_kind_of Array, dates
assert_kind_of Time, dates.first
assert_equal @deep[:street][:state][:dates].first, dates.first
votes = state.votes
assert_kind_of Array, votes
assert_kind_of TrueClass, votes.first
assert_equal @deep[:street][:state][:votes].first, votes.first
end
def test_nested_collections_within_the_same_namespace
n = Highrise::Note.new(:comments => [{ :comment => { :name => "1" } }])
assert_kind_of Highrise::Comment, n.comments.first
end
def test_nested_collections_within_deeply_nested_namespace
n = Highrise::Deeply::Nested::Note.new(:comments => [{ :name => "1" }])
assert_kind_of Highrise::Deeply::Nested::Comment, n.comments.first
end
def test_nested_collections_in_different_levels_of_namespaces
n = Highrise::Deeply::Nested::TestDifferentLevels::Note.new(:comments => [{ :name => "1" }])
assert_kind_of Highrise::Deeply::Nested::Comment, n.comments.first
end
end
require 'abstract_unit'
require 'active_support/core_ext/hash/conversions'
require "fixtures/person"
require "fixtures/street_address"
########################################################################
# Testing the schema of your Active Resource models
########################################################################
class SchemaTest < ActiveModel::TestCase
def setup
setup_response # find me in abstract_unit
end
def teardown
Person.schema = nil # hack to stop test bleedthrough...
end
#####################################################
# Passing in a schema directly and returning it
####
test "schema on a new model should be empty" do
assert Person.schema.blank?, "should have a blank class schema"
assert Person.new.schema.blank?, "should have a blank instance schema"
end
test "schema should only accept a hash" do
["blahblah", ['one','two'], [:age, :name], Person.new].each do |bad_schema|
assert_raises(ArgumentError,"should only accept a hash (or nil), but accepted: #{bad_schema.inspect}") do
Person.schema = bad_schema
end
end
end
test "schema should accept a simple hash" do
new_schema = {'age' => 'integer', 'name' => 'string',
'height' => 'float', 'bio' => 'text',
'weight' => 'decimal', 'photo' => 'binary',
'alive' => 'boolean', 'created_at' => 'timestamp',
'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'}
assert_nothing_raised { Person.schema = new_schema }
assert_equal new_schema, Person.schema
end
test "schema should accept a hash with simple values" do
new_schema = {'age' => 'integer', 'name' => 'string',
'height' => 'float', 'bio' => 'text',
'weight' => 'decimal', 'photo' => 'binary',
'alive' => 'boolean', 'created_at' => 'timestamp',
'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'}
assert_nothing_raised { Person.schema = new_schema }
assert_equal new_schema, Person.schema
end
test "schema should accept all known attribute types as values" do
# I'd prefer to use here...
ActiveResource::Schema::KNOWN_ATTRIBUTE_TYPES.each do |the_type|
assert_nothing_raised("should have accepted #{the_type.inspect}"){ Person.schema = {'my_key' => the_type }}
end
end
test "schema should not accept unknown values" do
bad_values = [ :oogle, :blob, 'thing']
bad_values.each do |bad_value|
assert_raises(ArgumentError,"should only accept a known attribute type, but accepted: #{bad_value.inspect}") do
Person.schema = {'key' => bad_value}
end
end
end
test "schema should accept nil and remove the schema" do
new_schema = {'age' => 'integer', 'name' => 'string',
'height' => 'float', 'bio' => 'text',
'weight' => 'decimal', 'photo' => 'binary',
'alive' => 'boolean', 'created_at' => 'timestamp',
'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'}
assert_nothing_raised { Person.schema = new_schema }
assert_equal new_schema, Person.schema # sanity check
assert_nothing_raised { Person.schema = nil }
assert_nil Person.schema, "should have nulled out the schema, but still had: #{Person.schema.inspect}"
end
test "schema should be with indifferent access" do
new_schema = {'age' => 'integer', 'name' => 'string',
'height' => 'float', 'bio' => 'text',
'weight' => 'decimal', 'photo' => 'binary',
'alive' => 'boolean', 'created_at' => 'timestamp',
'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'}
new_schema_syms = new_schema.keys
assert_nothing_raised { Person.schema = new_schema }
new_schema_syms.each do |col|
assert Person.new.respond_to?(col.to_s), "should respond to the schema's string key, but failed on: #{col.to_s}"
assert Person.new.respond_to?(col.to_sym), "should respond to the schema's symbol key, but failed on: #{col.to_sym}"
end
end
test "schema on a fetched resource should return all the attributes of that model instance" do
p = Person.find(1)
s = p.schema
assert s.present?, "should have found a non-empty schema!"
p.attributes.each do |the_attr, val|
assert s.has_key?(the_attr), "should have found attr: #{the_attr} in schema, but only had: #{s.inspect}"
end
end
test "with two instances, default schema should match the attributes of the individual instances - even if they differ" do
matz = Person.find(1)
rick = Person.find(6)
m_attrs = matz.attributes.keys.sort
r_attrs = rick.attributes.keys.sort
assert_not_equal m_attrs, r_attrs, "should have different attributes on each model"
assert_not_equal matz.schema, rick.schema, "should have had different schemas too"
end
test "defining a schema should return it when asked" do
assert Person.schema.blank?, "should have a blank class schema"
new_schema = {'age' => 'integer', 'name' => 'string',
'height' => 'float', 'bio' => 'text',
'weight' => 'decimal', 'photo' => 'binary',
'alive' => 'boolean', 'created_at' => 'timestamp',
'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'}
assert_nothing_raised {
Person.schema = new_schema
assert_equal new_schema, Person.schema, "should have saved the schema on the class"
assert_equal new_schema, Person.new.schema, "should have made the schema available to every instance"
}
end
test "defining a schema, then fetching a model should still match the defined schema" do
# sanity checks
assert Person.schema.blank?, "should have a blank class schema"
new_schema = {'age' => 'integer', 'name' => 'string',
'height' => 'float', 'bio' => 'text',
'weight' => 'decimal', 'photo' => 'binary',
'alive' => 'boolean', 'created_at' => 'timestamp',
'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'}
matz = Person.find(1)
assert !matz.schema.blank?, "should have some sort of schema on an instance variable"
assert_not_equal new_schema, matz.schema, "should not have the class-level schema until it's been added to the class!"
assert_nothing_raised {
Person.schema = new_schema
assert_equal new_schema, matz.schema, "class-level schema should override instance-level schema"
}
end
#####################################################
# Using the schema syntax
####
test "should be able to use schema" do
assert_respond_to Person, :schema, "should at least respond to the schema method"
assert_nothing_raised("Should allow the schema to take a block") do
Person.schema { }
end
end
test "schema definition should store and return attribute set" do
assert_nothing_raised do
s = nil
Person.schema do
s = self
attribute :foo, :string
end
assert_respond_to s, :attrs, "should return attributes in theory"
assert_equal({'foo' => 'string' }, s.attrs, "should return attributes in practice")
end
end
test "should be able to add attributes through schema" do
assert_nothing_raised do
s = nil
Person.schema do
s = self
attribute('foo', 'string')
end
assert s.attrs.has_key?('foo'), "should have saved the attribute name"
assert_equal 'string', s.attrs['foo'], "should have saved the attribute type"
end
end
test "should convert symbol attributes to strings" do
assert_nothing_raised do
s = nil
Person.schema do
attribute(:foo, :integer)
s = self
end
assert s.attrs.has_key?('foo'), "should have saved the attribute name as a string"
assert_equal 'integer', s.attrs['foo'], "should have saved the attribute type as a string"
end
end
test "should be able to add all known attribute types" do
assert_nothing_raised do
ActiveResource::Schema::KNOWN_ATTRIBUTE_TYPES.each do |the_type|
s = nil
Person.schema do
s = self
attribute('foo', the_type)
end
assert s.attrs.has_key?('foo'), "should have saved the attribute name"
assert_equal the_type.to_s, s.attrs['foo'], "should have saved the attribute type of: #{the_type}"
end
end
end
test "attributes should not accept unknown values" do
bad_values = [ :oogle, :blob, 'thing']
bad_values.each do |bad_value|
s = nil
assert_raises(ArgumentError,"should only accept a known attribute type, but accepted: #{bad_value.inspect}") do
Person.schema do
s = self
attribute 'key', bad_value
end
end
assert !self.respond_to?(bad_value), "should only respond to a known attribute type, but accepted: #{bad_value.inspect}"
assert_raises(NoMethodError,"should only have methods for known attribute types, but accepted: #{bad_value.inspect}") do
Person.schema do
send bad_value, 'key'
end
end
end
end
test "should accept attribute types as the type's name as the method" do
ActiveResource::Schema::KNOWN_ATTRIBUTE_TYPES.each do |the_type|
s = nil
Person.schema do
s = self
send(the_type,'foo')
end
assert s.attrs.has_key?('foo'), "should now have saved the attribute name"
assert_equal the_type.to_s, s.attrs['foo'], "should have saved the attribute type of: #{the_type}"
end
end
test "should accept multiple attribute names for an attribute method" do
names = ['foo','bar','baz']
s = nil
Person.schema do
s = self
string(*names)
end
names.each do |the_name|
assert s.attrs.has_key?(the_name), "should now have saved the attribute name: #{the_name}"
assert_equal 'string', s.attrs[the_name], "should have saved the attribute as a string"
end
end
#####################################################
# What a schema does for us
####
# respond_to?
test "should respond positively to attributes that are only in the schema" do
new_attr_name = :my_new_schema_attribute
new_attr_name_two = :another_new_schema_attribute
assert Person.schema.blank?, "sanity check - should have a blank class schema"
assert !Person.new.respond_to?(new_attr_name), "sanity check - should not respond to the brand-new attribute yet"
assert !Person.new.respond_to?(new_attr_name_two), "sanity check - should not respond to the brand-new attribute yet"
assert_nothing_raised do
Person.schema = {new_attr_name.to_s => 'string'}
Person.schema { string new_attr_name_two }
end
assert_respond_to Person.new, new_attr_name, "should respond to the attribute in a passed-in schema, but failed on: #{new_attr_name}"
assert_respond_to Person.new, new_attr_name_two, "should respond to the attribute from the schema, but failed on: #{new_attr_name_two}"
end
test "should not care about ordering of schema definitions" do
new_attr_name = :my_new_schema_attribute
new_attr_name_two = :another_new_schema_attribute
assert Person.schema.blank?, "sanity check - should have a blank class schema"
assert !Person.new.respond_to?(new_attr_name), "sanity check - should not respond to the brand-new attribute yet"
assert !Person.new.respond_to?(new_attr_name_two), "sanity check - should not respond to the brand-new attribute yet"
assert_nothing_raised do
Person.schema { string new_attr_name_two }
Person.schema = {new_attr_name.to_s => 'string'}
end
assert_respond_to Person.new, new_attr_name, "should respond to the attribute in a passed-in schema, but failed on: #{new_attr_name}"
assert_respond_to Person.new, new_attr_name_two, "should respond to the attribute from the schema, but failed on: #{new_attr_name_two}"
end
# method_missing effects
test "should not give method_missing for attribute only in schema" do
new_attr_name = :another_new_schema_attribute
new_attr_name_two = :another_new_schema_attribute
assert Person.schema.blank?, "sanity check - should have a blank class schema"
assert_raises(NoMethodError, "should not have found the attribute: #{new_attr_name} as a method") do
Person.new.send(new_attr_name)
end
assert_raises(NoMethodError, "should not have found the attribute: #{new_attr_name_two} as a method") do
Person.new.send(new_attr_name_two)
end
Person.schema = {new_attr_name.to_s => :float}
Person.schema { string new_attr_name_two }
assert_nothing_raised do
Person.new.send(new_attr_name)
Person.new.send(new_attr_name_two)
end
end
########
# Known attributes
#
# Attributes can be known to be attributes even if they aren't actually
# 'set' on a particular instance.
# This will only differ from 'attributes' if a schema has been set.
test "new model should have no known attributes" do
assert Person.known_attributes.blank?, "should have no known attributes"
assert Person.new.known_attributes.blank?, "should have no known attributes on a new instance"
end
test "setting schema should set known attributes on class and instance" do
new_schema = {'age' => 'integer', 'name' => 'string',
'height' => 'float', 'bio' => 'text',
'weight' => 'decimal', 'photo' => 'binary',
'alive' => 'boolean', 'created_at' => 'timestamp',
'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'}
assert_nothing_raised { Person.schema = new_schema }
assert_equal new_schema.keys.sort, Person.known_attributes.sort
assert_equal new_schema.keys.sort, Person.new.known_attributes.sort
end
test "known attributes on a fetched resource should return all the attributes of the instance" do
p = Person.find(1)
attrs = p.known_attributes
assert attrs.present?, "should have found some attributes!"
p.attributes.each do |the_attr, val|
assert attrs.include?(the_attr), "should have found attr: #{the_attr} in known attributes, but only had: #{attrs.inspect}"
end
end
test "with two instances, known attributes should match the attributes of the individual instances - even if they differ" do
matz = Person.find(1)
rick = Person.find(6)
m_attrs = matz.attributes.keys.sort
r_attrs = rick.attributes.keys.sort
assert_not_equal m_attrs, r_attrs, "should have different attributes on each model"
assert_not_equal matz.known_attributes, rick.known_attributes, "should have had different known attributes too"
end
test "setting schema then fetching should add schema attributes to the instance attributes" do
# an attribute in common with fetched instance and one that isn't
new_schema = {'age' => 'integer', 'name' => 'string',
'height' => 'float', 'bio' => 'text',
'weight' => 'decimal', 'photo' => 'binary',
'alive' => 'boolean', 'created_at' => 'timestamp',
'thetime' => 'time', 'thedate' => 'date', 'mydatetime' => 'datetime'}
assert_nothing_raised { Person.schema = new_schema }
matz = Person.find(1)
known_attrs = matz.known_attributes
matz.attributes.keys.each do |the_attr|
assert known_attrs.include?(the_attr), "should have found instance attr: #{the_attr} in known attributes, but only had: #{known_attrs.inspect}"
end
new_schema.keys.each do |the_attr|
assert known_attrs.include?(the_attr), "should have found schema attr: #{the_attr} in known attributes, but only had: #{known_attrs.inspect}"
end
end
end
require 'abstract_unit'
require "fixtures/person"
class BaseErrorsTest < ActiveSupport::TestCase
def setup
ActiveResource::HttpMock.respond_to do |mock|
mock.post "/people.xml", {}, %q(<?xml version="1.0" encoding="UTF-8"?><errors><error>Age can't be blank</error><error>Name can't be blank</error><error>Name must start with a letter</error><error>Person quota full for today.</error></errors>), 422, {'Content-Type' => 'application/xml; charset=utf-8'}
mock.post "/people.json", {}, %q({"errors":{"age":["can't be blank"],"name":["can't be blank", "must start with a letter"],"person":["quota full for today."]}}), 422, {'Content-Type' => 'application/json; charset=utf-8'}
end
end
def test_should_mark_as_invalid
[ :json, :xml ].each do |format|
invalid_user_using_format(format) do
assert !@person.valid?
end
end
end
def test_should_parse_json_and_xml_errors
[ :json, :xml ].each do |format|
invalid_user_using_format(format) do
assert_kind_of ActiveResource::Errors, @person.errors
assert_equal 4, @person.errors.size
end
end
end
def test_should_parse_json_errors_when_no_errors_key
ActiveResource::HttpMock.respond_to do |mock|
mock.post "/people.json", {}, '{}', 422, {'Content-Type' => 'application/json; charset=utf-8'}
end
invalid_user_using_format(:json) do
assert_kind_of ActiveResource::Errors, @person.errors
assert_equal 0, @person.errors.size
end
end
def test_should_parse_errors_to_individual_attributes
[ :json, :xml ].each do |format|
invalid_user_using_format(format) do
assert @person.errors[:name].any?
assert_equal ["can't be blank"], @person.errors[:age]
assert_equal ["can't be blank", "must start with a letter"], @person.errors[:name]
assert_equal ["Person quota full for today."], @person.errors[:base]
end
end
end
def test_should_iterate_over_errors
[ :json, :xml ].each do |format|
invalid_user_using_format(format) do
errors = []
@person.errors.each { |attribute, message| errors << [attribute, message] }
assert errors.include?([:name, "can't be blank"])
end
end
end
def test_should_iterate_over_full_errors
[ :json, :xml ].each do |format|
invalid_user_using_format(format) do
errors = []
@person.errors.to_a.each { |message| errors << message }
assert errors.include?("Name can't be blank")
end
end
end
def test_should_format_full_errors
[ :json, :xml ].each do |format|
invalid_user_using_format(format) do
full = @person.errors.full_messages
assert full.include?("Age can't be blank")
assert full.include?("Name can't be blank")
assert full.include?("Name must start with a letter")
assert full.include?("Person quota full for today.")
end
end
end
def test_should_mark_as_invalid_when_content_type_is_unavailable_in_response_header
ActiveResource::HttpMock.respond_to do |mock|
mock.post "/people.xml", {}, %q(<?xml version="1.0" encoding="UTF-8"?><errors><error>Age can't be blank</error><error>Name can't be blank</error><error>Name must start with a letter</error><error>Person quota full for today.</error></errors>), 422, {}
mock.post "/people.json", {}, %q({"errors":{"age":["can't be blank"],"name":["can't be blank", "must start with a letter"],"person":["quota full for today."]}}), 422, {}
end
[ :json, :xml ].each do |format|
invalid_user_using_format(format) do
assert !@person.valid?
end
end
end
def test_should_parse_json_string_errors_with_an_errors_key
ActiveResource::HttpMock.respond_to do |mock|
mock.post "/people.json", {}, %q({"errors":["Age can't be blank", "Name can't be blank", "Name must start with a letter", "Person quota full for today."]}), 422, {'Content-Type' => 'application/json; charset=utf-8'}
end
assert_deprecated(/as an array/) do
invalid_user_using_format(:json) do
assert @person.errors[:name].any?
assert_equal ["can't be blank"], @person.errors[:age]
assert_equal ["can't be blank", "must start with a letter"], @person.errors[:name]
assert_equal ["Person quota full for today."], @person.errors[:base]
end
end
end
def test_should_parse_3_1_style_json_errors
ActiveResource::HttpMock.respond_to do |mock|
mock.post "/people.json", {}, %q({"age":["can't be blank"],"name":["can't be blank", "must start with a letter"],"person":["quota full for today."]}), 422, {'Content-Type' => 'application/json; charset=utf-8'}
end
assert_deprecated(/without a root/) do
invalid_user_using_format(:json) do
assert @person.errors[:name].any?
assert_equal ["can't be blank"], @person.errors[:age]
assert_equal ["can't be blank", "must start with a letter"], @person.errors[:name]
assert_equal ["Person quota full for today."], @person.errors[:base]
end
end
end
private
def invalid_user_using_format(mime_type_reference)
previous_format = Person.format
Person.format = mime_type_reference
@person = Person.new(:name => '', :age => '')
assert_equal false, @person.save
yield
ensure
Person.format = previous_format
end
end
此差异已折叠。
此差异已折叠。
此差异已折叠。
require 'abstract_unit'
require "fixtures/person"
require "fixtures/street_address"
class FormatTest < ActiveSupport::TestCase
def setup
@matz = { :id => 1, :name => 'Matz' }
@david = { :id => 2, :name => 'David' }
@programmers = [ @matz, @david ]
end
def test_http_format_header_name
[:get, :head].each do |verb|
header_name = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[verb]
assert_equal 'Accept', header_name
end
[:patch, :put, :post].each do |verb|
header_name = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[verb]
assert_equal 'Content-Type', header_name
end
end
def test_formats_on_single_element
[ :json, :xml ].each do |format|
using_format(Person, format) do
ActiveResource::HttpMock.respond_to.get "/people/1.#{format}", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode(@david)
assert_equal @david[:name], Person.find(1).name
end
end
end
def test_formats_on_collection
[ :json, :xml ].each do |format|
using_format(Person, format) do
ActiveResource::HttpMock.respond_to.get "/people.#{format}", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode(@programmers)
remote_programmers = Person.find(:all)
assert_equal 2, remote_programmers.size
assert remote_programmers.find { |p| p.name == 'David' }
end
end
end
def test_formats_on_custom_collection_method
[ :json, :xml ].each do |format|
using_format(Person, format) do
ActiveResource::HttpMock.respond_to.get "/people/retrieve.#{format}?name=David", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode([@david])
remote_programmers = Person.get(:retrieve, :name => 'David')
assert_equal 1, remote_programmers.size
assert_equal @david[:id], remote_programmers[0]['id']
assert_equal @david[:name], remote_programmers[0]['name']
end
end
end
def test_formats_on_custom_element_method
[:json, :xml].each do |format|
using_format(Person, format) do
david = (format == :json ? { :person => @david } : @david)
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/2.#{format}", { 'Accept' => ActiveResource::Formats[format].mime_type }, ActiveResource::Formats[format].encode(david)
mock.get "/people/2/shallow.#{format}", { 'Accept' => ActiveResource::Formats[format].mime_type }, ActiveResource::Formats[format].encode(david)
end
remote_programmer = Person.find(2).get(:shallow)
assert_equal @david[:id], remote_programmer['id']
assert_equal @david[:name], remote_programmer['name']
end
ryan_hash = { :name => 'Ryan' }
ryan_hash = (format == :json ? { :person => ryan_hash } : ryan_hash)
ryan = ActiveResource::Formats[format].encode(ryan_hash)
using_format(Person, format) do
remote_ryan = Person.new(:name => 'Ryan')
ActiveResource::HttpMock.respond_to.post "/people.#{format}", { 'Content-Type' => ActiveResource::Formats[format].mime_type}, ryan, 201, { 'Location' => "/people/5.#{format}" }
remote_ryan.save
remote_ryan = Person.new(:name => 'Ryan')
ActiveResource::HttpMock.respond_to.post "/people/new/register.#{format}", { 'Content-Type' => ActiveResource::Formats[format].mime_type}, ryan, 201, { 'Location' => "/people/5.#{format}" }
assert_equal ActiveResource::Response.new(ryan, 201, { 'Location' => "/people/5.#{format}" }), remote_ryan.post(:register)
end
end
end
def test_setting_format_before_site
resource = Class.new(ActiveResource::Base)
resource.format = :json
resource.site = 'http://37s.sunrise.i:3000'
assert_equal ActiveResource::Formats[:json], resource.connection.format
end
def test_serialization_of_nested_resource
address = { :street => '12345 Street' }
person = { :name => 'Rus', :address => address}
[:json, :xml].each do |format|
encoded_person = ActiveResource::Formats[format].encode(person)
assert_match(/12345 Street/, encoded_person)
remote_person = Person.new(person.update({:address => StreetAddress.new(address)}))
assert_kind_of StreetAddress, remote_person.address
using_format(Person, format) do
ActiveResource::HttpMock.respond_to.post "/people.#{format}", {'Content-Type' => ActiveResource::Formats[format].mime_type}, encoded_person, 201, {'Location' => "/people/5.#{format}"}
remote_person.save
end
end
end
private
def using_format(klass, mime_type_reference)
previous_format = klass.format
klass.format = mime_type_reference
yield
ensure
klass.format = previous_format
end
end
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
class BeastResource < ActiveResource::Base
self.site = 'http://beast.caboo.se'
site.user = 'foo'
site.password = 'bar'
end
class Forum < BeastResource
# taken from BeastResource
# self.site = 'http://beast.caboo.se'
end
class Topic < BeastResource
self.site += '/forums/:forum_id'
end
class Customer < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000"
end
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册