Permalink
Browse files

Basic validation support [Rick Olson]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@5068 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent 7c4b6a5 commit 8d9e6609f8f67e55bba1f9bdbea62af22360dd3c @technoweenie technoweenie committed Sep 8, 2006
@@ -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' },
@@ -35,4 +35,11 @@
end
require 'active_resource/base'
-require 'active_resource/struct'
+require 'active_resource/struct'
+require 'active_resource/validations'
+
+module ActiveResource
+ Base.class_eval do
+ include Validations
+ end
+end
@@ -71,7 +71,9 @@ def handle_response(response)
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))
@@ -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
@@ -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", "<?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>", 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
@@ -17,8 +17,11 @@ def test_handle_response
# 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

0 comments on commit 8d9e660

Please sign in to comment.