From c2f90d6530dfd0ed68df9f4c429d0f498235e1d4 Mon Sep 17 00:00:00 2001 From: taryn Date: Wed, 19 Aug 2009 11:57:50 +0100 Subject: Added validations to ActiveResource. Added a smoke test to see if we can add a validation and use it, and add a validates callback and use it. Signed-off-by: Joshua Peek --- activeresource/lib/active_resource/validations.rb | 64 +++++++++++++++++------ activeresource/test/fixtures/project.rb | 25 +++++++++ activeresource/test/validations_test.rb | 49 +++++++++++++++++ 3 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 activeresource/test/fixtures/project.rb create mode 100644 activeresource/test/validations_test.rb diff --git a/activeresource/lib/active_resource/validations.rb b/activeresource/lib/active_resource/validations.rb index 4ff7be6a9e..d4d282e273 100644 --- a/activeresource/lib/active_resource/validations.rb +++ b/activeresource/lib/active_resource/validations.rb @@ -8,8 +8,10 @@ module ActiveResource # 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 @@ module ActiveResource 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 @@ module ActiveResource # 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 valid? 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 @@ module ActiveResource # 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 diff --git a/activeresource/test/fixtures/project.rb b/activeresource/test/fixtures/project.rb new file mode 100644 index 0000000000..e15fa6f620 --- /dev/null +++ b/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 + diff --git a/activeresource/test/validations_test.rb b/activeresource/test/validations_test.rb new file mode 100644 index 0000000000..f5a43b1ac1 --- /dev/null +++ b/activeresource/test/validations_test.rb @@ -0,0 +1,49 @@ +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_validate_callback + # we have a callback ensuring the description is longer thn 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 + -- cgit v1.2.3