From 8d9e6609f8f67e55bba1f9bdbea62af22360dd3c Mon Sep 17 00:00:00 2001 From: Rick Olson Date: Fri, 8 Sep 2006 00:07:30 +0000 Subject: Basic validation support [Rick Olson] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@5068 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activeresource/CHANGELOG | 6 ++ activeresource/lib/active_resource.rb | 9 +- activeresource/lib/active_resource/connection.rb | 4 +- activeresource/lib/active_resource/validations.rb | 125 ++++++++++++++++++++++ activeresource/test/base_errors_test.rb | 38 +++++++ activeresource/test/connection_test.rb | 5 +- 6 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 activeresource/lib/active_resource/validations.rb create mode 100644 activeresource/test/base_errors_test.rb diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index 2133824488..ef3f3f6a17 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -1,5 +1,11 @@ *SVN* +* 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' }, diff --git a/activeresource/lib/active_resource.rb b/activeresource/lib/active_resource.rb index 529675a59c..89fd6f7722 100644 --- a/activeresource/lib/active_resource.rb +++ b/activeresource/lib/active_resource.rb @@ -35,4 +35,11 @@ unless defined?(ActiveSupport) end require 'active_resource/base' -require 'active_resource/struct' \ No newline at end of file +require 'active_resource/struct' +require 'active_resource/validations' + +module ActiveResource + Base.class_eval do + include Validations + end +end \ No newline at end of file diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb index 22689444b2..132e861292 100644 --- a/activeresource/lib/active_resource/connection.rb +++ b/activeresource/lib/active_resource/connection.rb @@ -71,7 +71,9 @@ module ActiveResource response when 404 raise(ResourceNotFound.new(response)) - when 400...500 + when 400 + raise(ResourceInvalid.new(response)) + when 401...500 raise(ClientError.new(response)) when 500...600 raise(ServerError.new(response)) diff --git a/activeresource/lib/active_resource/validations.rb b/activeresource/lib/active_resource/validations.rb new file mode 100644 index 0000000000..8414e93293 --- /dev/null +++ b/activeresource/lib/active_resource/validations.rb @@ -0,0 +1,125 @@ +module ActiveResource + class ResourceInvalid < ClientError + end + + class Errors + include Enumerable + attr_reader :errors + + delegate :empty?, :to => :errors + + def initialize(base) # :nodoc: + @base, @errors = base, {} + end + + def add_to_base(msg) + add(:base, msg) + end + + def add(attribute, msg) + @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? + @errors[attribute.to_s] << msg + end + + # Returns true if the specified +attribute+ has errors associated with it. + def invalid?(attribute) + !@errors[attribute.to_s].nil? + end + + # * Returns nil, if no errors are associated with the specified +attribute+. + # * Returns the error message, if one error is associated with the specified +attribute+. + # * Returns an array of error messages, if more than one error is associated with the specified +attribute+. + def on(attribute) + errors = @errors[attribute.to_s] + return nil if errors.nil? + errors.size == 1 ? errors.first : errors + end + + alias :[] :on + + # Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute). + def on_base + on(:base) + end + + # Yields each attribute and associated message per error added. + def each + @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } + end + + # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned + # through iteration as "First name can't be empty". + def each_full + full_messages.each { |msg| yield msg } + end + + # Returns all the full error messages in an array. + def full_messages + full_messages = [] + + @errors.each_key do |attr| + @errors[attr].each do |msg| + next if msg.nil? + + if attr == "base" + full_messages << msg + else + full_messages << [attr.humanize, msg].join(' ') + end + end + end + full_messages + end + + def clear + @errors = {} + end + + # Returns the total number of errors added. Two errors added to the same attribute will be counted as such + # with this as well. + def size + @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size } + end + + alias_method :count, :size + alias_method :length, :size + + def from_xml(xml) + clear + humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) } + messages = Hash.create_from_xml(xml)['errors']['error'] rescue [] + 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 + + add_to_base message if attr_message.nil? + end + end + end + + module Validations + def self.included(base) # :nodoc: + base.class_eval do + alias_method_chain :save, :validation + end + end + + def save_with_validation + save_without_validation + rescue ResourceInvalid + errors.from_xml($!.response.body) + end + + def valid? + errors.empty? + end + + # Returns the Errors object that holds all information about attribute error messages. + def errors + @errors ||= Errors.new(self) + end + end +end \ No newline at end of file diff --git a/activeresource/test/base_errors_test.rb b/activeresource/test/base_errors_test.rb new file mode 100644 index 0000000000..25a368535f --- /dev/null +++ b/activeresource/test/base_errors_test.rb @@ -0,0 +1,38 @@ +require "#{File.dirname(__FILE__)}/abstract_unit" +require "fixtures/person" + +class BaseErrorsTest < Test::Unit::TestCase + def setup + ActiveResource::HttpMock.respond_to do |mock| + mock.post "/people", "Age can't be blankName can't be blankName must start with a letterPerson quota full for today.", 400 + end + @exception = nil + @person = Person.new(:name => '', :age => '') + @person.save + rescue ActiveResource::ResourceInvalid + @exception = $! + end + + def test_should_mark_as_invalid + assert !@person.valid? + end + + def test_should_parse_xml_errors + assert_kind_of ActiveResource::Errors, @person.errors + assert_equal 4, @person.errors.size + end + + def test_should_parse_errors_to_individual_attributes + assert_equal "can't be blank", @person.errors.on(:age) + assert_equal ["can't be blank", "must start with a letter"], @person.errors[:name] + assert_equal "Person quota full for today.", @person.errors.on_base + end + + def test_should_format_full_errors + 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 \ No newline at end of file diff --git a/activeresource/test/connection_test.rb b/activeresource/test/connection_test.rb index 4790c30bed..d72c1c612d 100644 --- a/activeresource/test/connection_test.rb +++ b/activeresource/test/connection_test.rb @@ -17,8 +17,11 @@ class ConnectionTest < Test::Unit::TestCase # 404 is a missing resource. assert_response_raises ActiveResource::ResourceNotFound, 404 + # 400 is a validation error + assert_response_raises ActiveResource::ResourceInvalid, 400 + # 4xx are client errors. - [400, 499].each do |code| + [401, 499].each do |code| assert_response_raises ActiveResource::ClientError, code end -- cgit v1.2.3