diff options
Diffstat (limited to 'activemodel/lib/active_model/errors.rb')
-rw-r--r-- | activemodel/lib/active_model/errors.rb | 149 |
1 files changed, 99 insertions, 50 deletions
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 22ca3efa2b..aba6618b56 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -1,16 +1,13 @@ # -*- coding: utf-8 -*- -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/hash/reverse_merge' -require 'active_support/ordered_hash' module ActiveModel # == Active Model Errors # - # Provides a modified +OrderedHash+ that you can include in your object + # Provides a modified +Hash+ that you can include in your object # for handling error messages and interacting with Action Pack helpers. # # A minimal implementation could be: @@ -49,8 +46,8 @@ module ActiveModel # # The last three methods are required in your object for Errors to be # able to generate error messages correctly and also handle multiple - # languages. Of course, if you extend your object with ActiveModel::Translations - # you will not need to implement the last two. Likewise, using + # languages. Of course, if you extend your object with ActiveModel::Translation + # you will not need to implement the last two. Likewise, using # ActiveModel::Validations will handle the validation related methods # for you. # @@ -63,7 +60,7 @@ module ActiveModel class Errors include Enumerable - CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank] + CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict] attr_reader :messages @@ -76,7 +73,12 @@ module ActiveModel # end def initialize(base) @base = base - @messages = ActiveSupport::OrderedHash.new + @messages = {} + end + + def initialize_dup(other) + @messages = other.messages.dup + super end # Clear the messages @@ -86,8 +88,9 @@ module ActiveModel # Do the error messages include an error with key +error+? def include?(error) - messages.include? error + (v = messages[error]) && v.any? end + alias :has_key? :include? # Get messages for +key+ def get(key) @@ -99,6 +102,11 @@ module ActiveModel messages[key] = value end + # Delete messages for +key+ + def delete(key) + messages.delete(key) + end + # When passed a symbol or a name of a method, returns an array of errors # for the method. # @@ -113,20 +121,20 @@ module ActiveModel # p.errors[:name] = "must be set" # p.errors[:name] # => ['must be set'] def []=(attribute, error) - self[attribute.to_sym] << error + self[attribute] << error end # Iterates through each error key, value pair in the error messages hash. - # Yields the attribute and the error for that attribute. If the attribute + # Yields the attribute and the error for that attribute. If the attribute # has more than one error message, yields once for each error message. # # p.errors.add(:name, "can't be blank") - # p.errors.each do |attribute, errors_array| + # p.errors.each do |attribute, error| # # Will yield :name and "can't be blank" # end # # p.errors.add(:name, "must be specified") - # p.errors.each do |attribute, errors_array| + # p.errors.each do |attribute, error| # # Will yield :name and "can't be blank" # # then yield :name and "must be specified" # end @@ -174,11 +182,13 @@ module ActiveModel to_a.size end - # Returns true if there are any errors, false if not. + # Returns true if no errors are found, false otherwise. + # If the error message is a string it can be empty. def empty? - all? { |k, v| v && v.empty? } + all? { |k, v| v && v.empty? && !v.is_a?(String) } end alias_method :blank?, :empty? + # Returns an xml formatted representation of the Errors hash. # # p.errors.add(:name, "can't be blank") @@ -191,32 +201,39 @@ module ActiveModel # # <error>name must be specified</error> # # </errors> def to_xml(options={}) - to_a.to_xml options.reverse_merge(:root => "errors", :skip_types => true) + to_a.to_xml({ :root => "errors", :skip_types => true }.merge!(options)) end - # Returns an ActiveSupport::OrderedHash that can be used as the JSON representation for this object. + # Returns an Hash that can be used as the JSON representation for this object. + # Options: + # * <tt>:full_messages</tt> - determines if json object should contain + # full messages or not. Default: <tt>false</tt>. def as_json(options=nil) - to_hash + to_hash(options && options[:full_messages]) end - def to_hash - messages.dup + def to_hash(full_messages = false) + if full_messages + messages = {} + self.messages.each do |attribute, array| + messages[attribute] = array.map { |message| full_message(attribute, message) } + end + messages + else + self.messages.dup + end end - # Adds +message+ to the error messages on +attribute+, which will be returned on a call to - # <tt>on(attribute)</tt> for the same attribute. More than one error can be added to the same - # +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. + # Adds +message+ to the error messages on +attribute+. More than one error can be added to the same + # +attribute+. # If no +message+ is supplied, <tt>:invalid</tt> is assumed. # - # If +message+ is a symbol, it will be translated using the appropriate scope (see +translate_error+). + # If +message+ is a symbol, it will be translated using the appropriate scope (see +generate_message+). # If +message+ is a proc, it will be called, allowing for things like <tt>Time.now</tt> to be used within an error. def add(attribute, message = nil, options = {}) - message ||= :invalid - - if message.is_a?(Symbol) - message = generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS)) - elsif message.is_a?(Proc) - message = message.call + message = normalize_message(attribute, message, options) + if options[:strict] + raise ActiveModel::StrictValidationFailed, full_message(attribute, message) end self[attribute] << message @@ -239,6 +256,15 @@ module ActiveModel end end + # Returns true if an error on the attribute with the given message is present, false otherwise. + # +message+ is treated the same as for +add+. + # p.errors.add :name, :blank + # p.errors.added? :name, :blank # => true + def added?(attribute, message = nil, options = {}) + message = normalize_message(attribute, message, options) + self[attribute].include? message + end + # Returns all the full error messages in an array. # # class Company @@ -248,22 +274,24 @@ module ActiveModel # # company = Company.create(:address => '123 First St.') # company.errors.full_messages # => - # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] + # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"] def full_messages - map { |attribute, message| - if attribute == :base - message - else - attr_name = attribute.to_s.gsub('.', '_').humanize - attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) - - I18n.t(:"errors.format", { - :default => "%{attribute} %{message}", - :attribute => attr_name, - :message => message - }) - end - } + map { |attribute, message| full_message(attribute, message) } + end + + # Returns a full message for a given attribute. + # + # company.errors.full_message(:name, "is invalid") # => + # "Name is invalid" + def full_message(attribute, message) + return message if attribute == :base + attr_name = attribute.to_s.tr('.', '_').humanize + attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) + I18n.t(:"errors.format", { + :default => "%{attribute} %{message}", + :attribute => attr_name, + :message => message + }) end # Translates an error message in its default scope @@ -293,13 +321,17 @@ module ActiveModel def generate_message(attribute, type = :invalid, options = {}) type = options.delete(:message) if options[:message].is_a?(Symbol) - defaults = @base.class.lookup_ancestors.map do |klass| - [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}", - :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ] + if @base.class.respond_to?(:i18n_scope) + defaults = @base.class.lookup_ancestors.map do |klass| + [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}", + :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ] + end + else + defaults = [] end defaults << options.delete(:message) - defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" + defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope) defaults << :"errors.attributes.#{attribute}.#{type}" defaults << :"errors.messages.#{type}" @@ -314,9 +346,26 @@ module ActiveModel :model => @base.class.model_name.human, :attribute => @base.class.human_attribute_name(attribute), :value => value - }.merge(options) + }.merge!(options) I18n.translate(key, options) end + + private + def normalize_message(attribute, message, options) + message ||= :invalid + + case message + when Symbol + generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS)) + when Proc + message.call + else + message + end + end + end + + class StrictValidationFailed < StandardError end end |