Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge commit 'rails/master'

  • Loading branch information...
commit b3427286771bb476c0c4a58488033bd671740332 2 parents 2048556 + 328ba3b
@miloops miloops authored
View
56 activeresource/lib/active_resource/base.rb
@@ -585,6 +585,19 @@ def create(attributes = {})
#
# StreetAddress.find(1, :params => { :person_id => 1 })
# # => GET /people/1/street_addresses/1.xml
+ #
+ # == Failure or missing data
+ # A failure to find the requested object raises a ResourceNotFound
+ # exception if the find was called with an id.
+ # With any other scope, find returns nil when no data is returned.
+ #
+ # Person.find(1)
+ # # => raises ResourcenotFound
+ #
+ # Person.find(:all)
+ # Person.find(:first)
+ # Person.find(:last)
+ # # => nil
def find(*arguments)
scope = arguments.slice!(0)
options = arguments.slice!(0) || {}
@@ -638,16 +651,22 @@ def exists?(id, options = {})
private
# Find every resource
def find_every(options)
- case from = options[:from]
- when Symbol
- instantiate_collection(get(from, options[:params]))
- when String
- path = "#{from}#{query_string(options[:params])}"
- instantiate_collection(connection.get(path, headers) || [])
- else
- prefix_options, query_options = split_options(options[:params])
- path = collection_path(prefix_options, query_options)
- instantiate_collection( (connection.get(path, headers) || []), prefix_options )
+ begin
+ case from = options[:from]
+ when Symbol
+ instantiate_collection(get(from, options[:params]))
+ when String
+ path = "#{from}#{query_string(options[:params])}"
+ instantiate_collection(connection.get(path, headers) || [])
+ else
+ prefix_options, query_options = split_options(options[:params])
+ path = collection_path(prefix_options, query_options)
+ instantiate_collection( (connection.get(path, headers) || []), prefix_options )
+ end
+ rescue ActiveResource::ResourceNotFound
+ # Swallowing ResourceNotFound exceptions and return nil - as per
+ # ActiveRecord.
+ nil
end
end
@@ -874,6 +893,23 @@ def dup
def save
new? ? create : update
end
+
+ # Saves the resource.
+ #
+ # If the resource is new, it is created via +POST+, otherwise the
+ # existing resource is updated via +PUT+.
+ #
+ # With <tt>save!</tt> validations always run. If any of them fail
+ # ActiveResource::ResourceInvalid gets raised, and nothing is POSTed to
+ # the remote system.
+ # See ActiveResource::Validations for more information.
+ #
+ # There's a series of callbacks associated with <tt>save!</tt>. If any
+ # of the <tt>before_*</tt> callbacks return +false+ the action is
+ # cancelled and <tt>save!</tt> raises ActiveResource::ResourceInvalid.
+ def save!
+ save || raise(ResourceInvalid.new(self))
+ end
# Deletes the resource from the remote service.
#
View
64 activeresource/lib/active_resource/validations.rb
@@ -8,8 +8,10 @@ class ResourceInvalid < ClientError #:nodoc:
# 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)
- def from_array(messages)
- clear
+ # 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 = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) }
messages.each do |message|
attr_message = humanized_attributes.keys.detect do |attr_name|
@@ -22,16 +24,16 @@ def from_array(messages)
end
end
- # Grabs errors from the json response.
- def from_json(json)
+ # Grabs errors from a json response.
+ def from_json(json, save_cache = false)
array = ActiveSupport::JSON.decode(json)['errors'] rescue []
- from_array array
+ from_array array, save_cache
end
- # Grabs errors from the XML response.
- def from_xml(xml)
+ # 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
+ from_array array, save_cache
end
end
@@ -57,26 +59,55 @@ def from_xml(xml)
#
module Validations
extend ActiveSupport::Concern
+ include ActiveModel::Validations
+ extend ActiveModel::Validations::ClassMethods
included do
alias_method_chain :save, :validation
end
# Validate a resource and save (POST) it to the remote web service.
- def save_with_validation
- save_without_validation
- true
+ # If any local validations fail - the save (POST) will not be attempted.
+ def save_with_validation(perform_validation = true)
+ # 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
- case error.response['Content-Type']
+ # 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 remote_errors.response['Content-Type']
when 'application/xml'
- errors.from_xml(error.response.body)
+ errors.from_xml(remote_errors.response.body, save_cache)
when 'application/json'
- errors.from_json(error.response.body)
+ errors.from_json(remote_errors.response.body, save_cache)
end
- false
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])
@@ -86,7 +117,10 @@ def save_with_validation
# 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
View
0  activeresource/test/authorization_test.rb → activeresource/test/cases/authorization_test.rb
File renamed without changes
View
0  activeresource/test/base/custom_methods_test.rb → ...veresource/test/cases/base/custom_methods_test.rb
File renamed without changes
View
0  activeresource/test/base/equality_test.rb → activeresource/test/cases/base/equality_test.rb
File renamed without changes
View
0  activeresource/test/base/load_test.rb → activeresource/test/cases/base/load_test.rb
File renamed without changes
View
0  activeresource/test/base_errors_test.rb → activeresource/test/cases/base_errors_test.rb
File renamed without changes
View
82 activeresource/test/base_test.rb → activeresource/test/cases/base_test.rb
@@ -637,13 +637,6 @@ def test_custom_prefix
assert_equal [:person_id].to_set, StreetAddress.__send__(:prefix_parameters)
end
- def test_find_by_id
- matz = Person.find(1)
- assert_kind_of Person, matz
- assert_equal "Matz", matz.name
- assert matz.name?
- end
-
def test_respond_to
matz = Person.find(1)
assert matz.respond_to?(:name)
@@ -652,32 +645,6 @@ def test_respond_to
assert !matz.respond_to?(:super_scalable_stuff)
end
- def test_find_by_id_with_custom_prefix
- addy = StreetAddress.find(1, :params => { :person_id => 1 })
- assert_kind_of StreetAddress, addy
- assert_equal '12345 Street', addy.street
- end
-
- def test_find_all
- all = Person.find(:all)
- assert_equal 2, all.size
- assert_kind_of Person, all.first
- assert_equal "Matz", all.first.name
- assert_equal "David", all.last.name
- end
-
- def test_find_first
- matz = Person.find(:first)
- assert_kind_of Person, matz
- assert_equal "Matz", matz.name
- end
-
- def test_find_last
- david = Person.find(:last)
- assert_kind_of Person, david
- assert_equal 'David', david.name
- end
-
def test_custom_header
Person.headers['key'] = 'value'
assert_raise(ActiveResource::ResourceNotFound) { Person.find(4) }
@@ -685,52 +652,15 @@ def test_custom_header
Person.headers.delete('key')
end
- def test_find_by_id_not_found
- assert_raise(ActiveResource::ResourceNotFound) { Person.find(99) }
- assert_raise(ActiveResource::ResourceNotFound) { StreetAddress.find(1) }
- end
-
- def test_find_all_by_from
- ActiveResource::HttpMock.respond_to { |m| m.get "/companies/1/people.xml", {}, @people_david }
-
- people = Person.find(:all, :from => "/companies/1/people.xml")
- assert_equal 1, people.size
- assert_equal "David", people.first.name
- end
-
- def test_find_all_by_from_with_options
- ActiveResource::HttpMock.respond_to { |m| m.get "/companies/1/people.xml", {}, @people_david }
-
- people = Person.find(:all, :from => "/companies/1/people.xml")
- assert_equal 1, people.size
- assert_equal "David", people.first.name
- end
-
- def test_find_all_by_symbol_from
- ActiveResource::HttpMock.respond_to { |m| m.get "/people/managers.xml", {}, @people_david }
-
- people = Person.find(:all, :from => :managers)
- assert_equal 1, people.size
- assert_equal "David", people.first.name
- end
-
- def test_find_single_by_from
- ActiveResource::HttpMock.respond_to { |m| m.get "/companies/1/manager.xml", {}, @david }
-
- david = Person.find(:one, :from => "/companies/1/manager.xml")
- assert_equal "David", david.name
- end
-
- def test_find_single_by_symbol_from
- ActiveResource::HttpMock.respond_to { |m| m.get "/people/leader.xml", {}, @david }
-
- david = Person.find(:one, :from => :leader)
- assert_equal "David", david.name
+ def test_save
+ rick = Person.new
+ assert rick.save
+ assert_equal '5', rick.id
end
- def test_save
+ def test_save!
rick = Person.new
- assert_equal true, rick.save
+ assert rick.save!
assert_equal '5', rick.id
end
View
195 activeresource/test/cases/finder_test.rb
@@ -0,0 +1,195 @@
+require 'abstract_unit'
+require "fixtures/person"
+require "fixtures/customer"
+require "fixtures/street_address"
+require "fixtures/beast"
+require "fixtures/proxy"
+require 'active_support/core_ext/hash/conversions'
+
+class FinderTest < Test::Unit::TestCase
+ def setup
+ # TODO: refactor/DRY this setup - it's a copy of the BaseTest setup.
+ # We can probably put this into abstract_unit
+ @matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
+ @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
+ @greg = { :id => 3, :name => 'Greg' }.to_xml(:root => 'person')
+ @addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address')
+ @default_request_headers = { 'Content-Type' => 'application/xml' }
+ @rick = { :name => "Rick", :age => 25 }.to_xml(:root => "person")
+ @people = [{ :id => 1, :name => 'Matz' }, { :id => 2, :name => 'David' }].to_xml(:root => 'people')
+ @people_david = [{ :id => 2, :name => 'David' }].to_xml(:root => 'people')
+ @addresses = [{ :id => 1, :street => '12345 Street' }].to_xml(:root => 'addresses')
+
+ # - 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 = {: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_xml(:root => 'customer')
+ # - resource with yaml array of strings; for ActiveRecords 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
+
+ ActiveResource::HttpMock.respond_to do |mock|
+ mock.get "/people/1.xml", {}, @matz
+ mock.get "/people/2.xml", {}, @david
+ mock.get "/people/5.xml", {}, @marty
+ mock.get "/people/Greg.xml", {}, @greg
+ mock.get "/people/4.xml", {'key' => 'value'}, nil, 404
+ mock.put "/people/1.xml", {}, nil, 204
+ mock.delete "/people/1.xml", {}, nil, 200
+ mock.delete "/people/2.xml", {}, nil, 400
+ mock.get "/people/99.xml", {}, nil, 404
+ mock.post "/people.xml", {}, @rick, 201, 'Location' => '/people/5.xml'
+ mock.get "/people.xml", {}, @people
+ mock.get "/people/1/addresses.xml", {}, @addresses
+ mock.get "/people/1/addresses/1.xml", {}, @addy
+ mock.get "/people/1/addresses/2.xml", {}, nil, 404
+ mock.get "/people/2/addresses.xml", {}, nil, 404
+ mock.get "/people/2/addresses/1.xml", {}, nil, 404
+ mock.get "/people/Greg/addresses/1.xml", {}, @addy
+ mock.put "/people/1/addresses/1.xml", {}, nil, 204
+ mock.delete "/people/1/addresses/1.xml", {}, nil, 200
+ mock.post "/people/1/addresses.xml", {}, nil, 201, 'Location' => '/people/1/addresses/5'
+ mock.get "/people//addresses.xml", {}, nil, 404
+ mock.get "/people//addresses/1.xml", {}, nil, 404
+ mock.put "/people//addresses/1.xml", {}, nil, 404
+ mock.delete "/people//addresses/1.xml", {}, nil, 404
+ mock.post "/people//addresses.xml", {}, nil, 404
+ mock.head "/people/1.xml", {}, nil, 200
+ mock.head "/people/Greg.xml", {}, nil, 200
+ mock.head "/people/99.xml", {}, nil, 404
+ mock.head "/people/1/addresses/1.xml", {}, nil, 200
+ mock.head "/people/1/addresses/2.xml", {}, nil, 404
+ mock.head "/people/2/addresses/1.xml", {}, nil, 404
+ mock.head "/people/Greg/addresses/1.xml", {}, nil, 200
+ # customer
+ mock.get "/customers/1.xml", {}, @luis
+ end
+
+ Person.user = nil
+ Person.password = nil
+ end
+
+ def test_find_by_id
+ matz = Person.find(1)
+ assert_kind_of Person, matz
+ assert_equal "Matz", matz.name
+ assert matz.name?
+ end
+
+ def test_find_by_id_with_custom_prefix
+ addy = StreetAddress.find(1, :params => { :person_id => 1 })
+ assert_kind_of StreetAddress, addy
+ assert_equal '12345 Street', addy.street
+ end
+
+ def test_find_all
+ all = Person.find(:all)
+ assert_equal 2, all.size
+ assert_kind_of Person, all.first
+ assert_equal "Matz", all.first.name
+ assert_equal "David", all.last.name
+ end
+
+ def test_find_first
+ matz = Person.find(:first)
+ assert_kind_of Person, matz
+ assert_equal "Matz", matz.name
+ end
+
+ def test_find_last
+ david = Person.find(:last)
+ assert_kind_of Person, david
+ assert_equal 'David', david.name
+ end
+
+ def test_find_by_id_not_found
+ assert_raise(ActiveResource::ResourceNotFound) { Person.find(99) }
+ assert_raise(ActiveResource::ResourceNotFound) { StreetAddress.find(1) }
+ end
+
+ def test_find_all_sub_objects
+ all = StreetAddress.find(:all, :params => { :person_id => 1 })
+ assert_equal 1, all.size
+ assert_kind_of StreetAddress, all.first
+ end
+
+ def test_find_all_sub_objects_not_found
+ assert_nothing_raised do
+ addys = StreetAddress.find(:all, :params => { :person_id => 2 })
+ end
+ end
+
+ def test_find_all_by_from
+ ActiveResource::HttpMock.respond_to { |m| m.get "/companies/1/people.xml", {}, @people_david }
+
+ people = Person.find(:all, :from => "/companies/1/people.xml")
+ assert_equal 1, people.size
+ assert_equal "David", people.first.name
+ end
+
+ def test_find_all_by_from_with_options
+ ActiveResource::HttpMock.respond_to { |m| m.get "/companies/1/people.xml", {}, @people_david }
+
+ people = Person.find(:all, :from => "/companies/1/people.xml")
+ assert_equal 1, people.size
+ assert_equal "David", people.first.name
+ end
+
+ def test_find_all_by_symbol_from
+ ActiveResource::HttpMock.respond_to { |m| m.get "/people/managers.xml", {}, @people_david }
+
+ people = Person.find(:all, :from => :managers)
+ assert_equal 1, people.size
+ assert_equal "David", people.first.name
+ end
+
+ def test_find_single_by_from
+ ActiveResource::HttpMock.respond_to { |m| m.get "/companies/1/manager.xml", {}, @david }
+
+ david = Person.find(:one, :from => "/companies/1/manager.xml")
+ assert_equal "David", david.name
+ end
+
+ def test_find_single_by_symbol_from
+ ActiveResource::HttpMock.respond_to { |m| m.get "/people/leader.xml", {}, @david }
+
+ david = Person.find(:one, :from => :leader)
+ assert_equal "David", david.name
+ end
+end
View
0  activeresource/test/format_test.rb → activeresource/test/cases/format_test.rb
File renamed without changes
View
0  activeresource/test/observing_test.rb → activeresource/test/cases/observing_test.rb
File renamed without changes
View
55 activeresource/test/cases/validations_test.rb
@@ -0,0 +1,55 @@
+require 'abstract_unit'
+require "fixtures/project"
+
+# The validations are tested thoroughly under ActiveModel::Validations
+# This test case simply makes sur that they are all accessible by
+# Active Resource objects.
+class ValidationsTest < ActiveModel::TestCase
+ VALID_PROJECT_HASH = { :name => "My Project", :description => "A project" }
+ def setup
+ @my_proj = VALID_PROJECT_HASH.to_xml(:root => "person")
+ ActiveResource::HttpMock.respond_to do |mock|
+ mock.post "/projects.xml", {}, @my_proj, 201, 'Location' => '/projects/5.xml'
+ end
+ end
+
+ def test_validates_presence_of
+ p = new_project(:name => nil)
+ assert !p.valid?, "should not be a valid record without name"
+ assert !p.save, "should not have saved an invalid record"
+ assert_equal ["can't be blank"], p.errors[:name], "should have an error on name"
+
+ p.name = "something"
+
+ assert p.save, "should have saved after fixing the validation, but had: #{p.errors.inspect}"
+ end
+
+ def test_fails_save!
+ p = new_project(:name => nil)
+ assert_raise(ActiveResource::ResourceInvalid) { p.save! }
+ end
+
+
+ def test_validate_callback
+ # we have a callback ensuring the description is longer than three letters
+ p = new_project(:description => 'a')
+ assert !p.valid?, "should not be a valid record when it fails a validation callback"
+ assert !p.save, "should not have saved an invalid record"
+ assert_equal ["must be greater than three letters long"], p.errors[:description], "should be an error on description"
+
+ # should now allow this description
+ p.description = 'abcd'
+ assert p.save, "should have saved after fixing the validation, but had: #{p.errors.inspect}"
+ end
+
+ protected
+
+ # quickie helper to create a new project with all the required
+ # attributes.
+ # Pass in any params you specifically want to override
+ def new_project(opts = {})
+ Project.new(VALID_PROJECT_HASH.merge(opts))
+ end
+
+end
+
View
25 activeresource/test/fixtures/project.rb
@@ -0,0 +1,25 @@
+# used to test validations
+class Project < ActiveResource::Base
+ self.site = "http://37s.sunrise.i:3000"
+
+ validates_presence_of :name
+ validate :description_greater_than_three_letters
+
+ # to test the validate *callback* works
+ def description_greater_than_three_letters
+ errors.add :description, 'must be greater than three letters long' if description.length < 3 unless description.blank?
+ end
+
+
+ # stop-gap accessor to default this attribute to nil
+ # Otherwise the validations fail saying that the method does not exist.
+ # In future, method_missing will be updated to not explode on a known
+ # attribute.
+ def name
+ attributes['name'] || nil
+ end
+ def description
+ attributes['description'] || nil
+ end
+end
+
View
8 ci/ci_build.rb
@@ -39,13 +39,6 @@
end
cd "#{root_dir}/activerecord" do
- puts
- puts "[CruiseControl] Building ActiveRecord with SQLite 2"
- puts
- build_results[:activerecord_sqlite] = system 'rake test_sqlite'
-end
-
-cd "#{root_dir}/activerecord" do
puts
puts "[CruiseControl] Building ActiveRecord with SQLite 3"
puts
@@ -96,7 +89,6 @@
puts "[CruiseControl] #{`ruby -v`}"
puts "[CruiseControl] #{`mysql --version`}"
puts "[CruiseControl] #{`pg_config --version`}"
-puts "[CruiseControl] SQLite2: #{`sqlite -version`}"
puts "[CruiseControl] SQLite3: #{`sqlite3 -version`}"
`gem env`.each_line {|line| print "[CruiseControl] #{line}"}
puts "[CruiseControl] Local gems:"
Please sign in to comment.
Something went wrong with that request. Please try again.