diff options
Diffstat (limited to 'activemodel/lib/active_model/errors.rb')
-rw-r--r-- | activemodel/lib/active_model/errors.rb | 297 |
1 files changed, 171 insertions, 126 deletions
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 29e0c977ce..9de6b609a3 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -1,9 +1,9 @@ -# -*- coding: utf-8 -*- +# frozen_string_literal: true -require 'active_support/core_ext/array/conversions' -require 'active_support/core_ext/string/inflections' -require 'active_support/core_ext/object/deep_dup' -require 'active_support/core_ext/string/filters' +require "active_support/core_ext/array/conversions" +require "active_support/core_ext/string/inflections" +require "active_support/core_ext/object/deep_dup" +require "active_support/core_ext/string/filters" module ActiveModel # == Active \Model \Errors @@ -62,6 +62,11 @@ module ActiveModel CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict] MESSAGE_OPTIONS = [:message] + class << self + attr_accessor :i18n_full_message # :nodoc: + end + self.i18n_full_message = false + attr_reader :messages, :details # Pass in the instance of the object that is using the errors object. @@ -73,8 +78,8 @@ module ActiveModel # end def initialize(base) @base = base - @messages = Hash.new { |messages, attribute| messages[attribute] = [] } - @details = Hash.new { |details, attribute| details[attribute] = [] } + @messages = apply_default_array({}) + @details = apply_default_array({}) end def initialize_dup(other) # :nodoc: @@ -83,6 +88,30 @@ module ActiveModel super end + # Copies the errors from <tt>other</tt>. + # + # other - The ActiveModel::Errors instance. + # + # Examples + # + # person.errors.copy!(other) + def copy!(other) # :nodoc: + @messages = other.messages.dup + @details = other.details.dup + end + + # Merges the errors from <tt>other</tt>. + # + # other - The ActiveModel::Errors instance. + # + # Examples + # + # person.errors.merge!(other) + def merge!(other) + @messages.merge!(other.messages) { |_, ary1, ary2| ary1 + ary2 } + @details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 } + end + # Clear the error messages. # # person.errors.full_messages # => ["name cannot be nil"] @@ -100,49 +129,21 @@ module ActiveModel # person.errors.include?(:name) # => true # person.errors.include?(:age) # => false def include?(attribute) - messages[attribute].present? + attribute = attribute.to_sym + messages.key?(attribute) && messages[attribute].present? end alias :has_key? :include? alias :key? :include? - # Get messages for +key+. - # - # person.errors.messages # => {:name=>["cannot be nil"]} - # person.errors.get(:name) # => ["cannot be nil"] - # person.errors.get(:age) # => [] - def get(key) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - ActiveModel::Errors#get is deprecated and will be removed in Rails 5.1. - - To achieve the same use model.errors[:#{key}]. - MESSAGE - - messages[key] - end - - # Set messages for +key+ to +value+. - # - # person.errors[:name] # => ["cannot be nil"] - # person.errors.set(:name, ["can't be nil"]) - # person.errors[:name] # => ["can't be nil"] - def set(key, value) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - ActiveModel::Errors#set is deprecated and will be removed in Rails 5.1. - - Use model.errors.add(:#{key}, #{value.inspect}) instead. - MESSAGE - - messages[key] = value - end - # Delete messages for +key+. Returns the deleted messages. # - # person.errors[:name] # => ["cannot be nil"] + # person.errors[:name] # => ["cannot be nil"] # person.errors.delete(:name) # => ["cannot be nil"] - # person.errors[:name] # => [] + # person.errors[:name] # => [] def delete(key) - details.delete(key) - messages.delete(key) + attribute = key.to_sym + details.delete(attribute) + messages.delete(attribute) end # When passed a symbol or a name of a method, returns an array of errors @@ -154,20 +155,6 @@ module ActiveModel messages[attribute.to_sym] end - # Adds to the supplied attribute the supplied error message. - # - # person.errors[:name] = "must be set" - # person.errors[:name] # => ['must be set'] - def []=(attribute, error) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - ActiveModel::Errors#[]= is deprecated and will be removed in Rails 5.1. - - Use model.errors.add(:#{attribute}, #{error.inspect}) instead. - MESSAGE - - messages[attribute.to_sym] << 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 # has more than one error message, yields once for each error message. @@ -204,7 +191,9 @@ module ActiveModel # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]} # person.errors.values # => [["cannot be nil", "must be specified"]] def values - messages.values + messages.select do |key, value| + !value.empty? + end.values end # Returns all message keys. @@ -212,7 +201,9 @@ module ActiveModel # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]} # person.errors.keys # => [:name] def keys - messages.keys + messages.select do |key, value| + !value.empty? + end.keys end # Returns +true+ if no errors are found, +false+ otherwise. @@ -236,7 +227,7 @@ module ActiveModel # # <error>name can't be blank</error> # # <error>name must be specified</error> # # </errors> - def to_xml(options={}) + def to_xml(options = {}) to_a.to_xml({ root: "errors", skip_types: true }.merge!(options)) end @@ -246,7 +237,7 @@ module ActiveModel # # person.errors.as_json # => {:name=>["cannot be nil"]} # person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]} - def as_json(options=nil) + def as_json(options = nil) to_hash(options && options[:full_messages]) end @@ -257,11 +248,11 @@ module ActiveModel # person.errors.to_hash(true) # => {:name=>["name cannot be nil"]} def to_hash(full_messages = false) if full_messages - self.messages.each_with_object({}) do |(attribute, array), messages| + messages.each_with_object({}) do |(attribute, array), messages| messages[attribute] = array.map { |message| full_message(attribute, message) } end else - self.messages.dup + without_default_proc(messages) end end @@ -291,9 +282,9 @@ module ActiveModel # <tt>:strict</tt> option can also be set to any other exception. # # person.errors.add(:name, :invalid, strict: true) - # # => ActiveModel::StrictValidationFailed: name is invalid + # # => ActiveModel::StrictValidationFailed: Name is invalid # person.errors.add(:name, :invalid, strict: NameIsInvalid) - # # => NameIsInvalid: name is invalid + # # => NameIsInvalid: Name is invalid # # person.errors.messages # => {} # @@ -308,7 +299,7 @@ module ActiveModel # # => {:base=>[{error: :name_or_email_blank}]} def add(attribute, message = :invalid, options = {}) message = message.call if message.respond_to?(:call) - detail = normalize_detail(attribute, message, options) + detail = normalize_detail(message, options) message = normalize_message(attribute, message, options) if exception = options[:strict] exception = ActiveModel::StrictValidationFailed if exception == true @@ -319,58 +310,30 @@ module ActiveModel messages[attribute.to_sym] << message end - # Will add an error message to each of the attributes in +attributes+ - # that is empty. - # - # person.errors.add_on_empty(:name) - # person.errors.messages - # # => {:name=>["can't be empty"]} - def add_on_empty(attributes, options = {}) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1 - - To achieve the same use: - - errors.add(attribute, :empty, options) if value.nil? || value.empty? - MESSAGE - - Array(attributes).each do |attribute| - value = @base.send(:read_attribute_for_validation, attribute) - is_empty = value.respond_to?(:empty?) ? value.empty? : false - add(attribute, :empty, options) if value.nil? || is_empty - end - end - - # Will add an error message to each of the attributes in +attributes+ that - # is blank (using Object#blank?). - # - # person.errors.add_on_blank(:name) - # person.errors.messages - # # => {:name=>["can't be blank"]} - def add_on_blank(attributes, options = {}) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1 - - To achieve the same use: - - errors.add(attribute, :empty, options) if value.blank? - MESSAGE - - Array(attributes).each do |attribute| - value = @base.send(:read_attribute_for_validation, attribute) - add(attribute, :blank, options) if value.blank? - 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+. + # present, or +false+ otherwise. +message+ is treated the same as for +add+. # # person.errors.add :name, :blank - # person.errors.added? :name, :blank # => true + # person.errors.added? :name, :blank # => true + # person.errors.added? :name, "can't be blank" # => true + # + # If the error message requires an option, then it returns +true+ with + # the correct option, or +false+ with an incorrect or missing option. + # + # person.errors.add :name, :too_long, { count: 25 } + # person.errors.added? :name, :too_long, count: 25 # => true + # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true + # person.errors.added? :name, :too_long, count: 24 # => false + # person.errors.added? :name, :too_long # => false + # person.errors.added? :name, "is too long" # => false def added?(attribute, message = :invalid, options = {}) message = message.call if message.respond_to?(:call) - message = normalize_message(attribute, message, options) - self[attribute].include? message + + if message.is_a? Symbol + details[attribute.to_sym].include? normalize_detail(message, options) + else + self[attribute].include? message + end end # Returns all the full error messages in an array. @@ -399,21 +362,63 @@ module ActiveModel # person.errors.full_messages_for(:name) # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"] def full_messages_for(attribute) + attribute = attribute.to_sym messages[attribute].map { |message| full_message(attribute, message) } end # Returns a full message for a given attribute. # # person.errors.full_message(:name, 'is invalid') # => "Name is invalid" + # + # The `"%{attribute} %{message}"` error format can be overridden with either + # + # * <tt>activemodel.errors.models.person/contacts/addresses.attributes.street.format</tt> + # * <tt>activemodel.errors.models.person/contacts/addresses.format</tt> + # * <tt>activemodel.errors.models.person.attributes.name.format</tt> + # * <tt>activemodel.errors.models.person.format</tt> + # * <tt>errors.format</tt> def full_message(attribute, message) return message if attribute == :base - attr_name = attribute.to_s.tr('.', '_').humanize + attribute = attribute.to_s + + if self.class.i18n_full_message && @base.class.respond_to?(:i18n_scope) + attribute = attribute.remove(/\[\d\]/) + parts = attribute.split(".") + attribute_name = parts.pop + namespace = parts.join("/") unless parts.empty? + attributes_scope = "#{@base.class.i18n_scope}.errors.models" + + if namespace + defaults = @base.class.lookup_ancestors.map do |klass| + [ + :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format", + :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format", + ] + end + else + defaults = @base.class.lookup_ancestors.map do |klass| + [ + :"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format", + :"#{attributes_scope}.#{klass.model_name.i18n_key}.format", + ] + end + end + + defaults.flatten! + else + defaults = [] + end + + defaults << :"errors.format" + defaults << "%{attribute} %{message}" + + attr_name = attribute.tr(".", "_").humanize attr_name = @base.class.human_attribute_name(attribute, default: attr_name) - I18n.t(:"errors.format", { - default: "%{attribute} %{message}", + + I18n.t(defaults.shift, + default: defaults, attribute: attr_name, - message: message - }) + message: message) end # Translates an error message in its default scope @@ -444,21 +449,19 @@ module ActiveModel type = options.delete(:message) if options[:message].is_a?(Symbol) 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}" ] + i18n_scope = @base.class.i18n_scope.to_s + defaults = @base.class.lookup_ancestors.flat_map do |klass| + [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}", + :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ] end + defaults << :"#{i18n_scope}.errors.messages.#{type}" else defaults = [] end - defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope) defaults << :"errors.attributes.#{attribute}.#{type}" defaults << :"errors.messages.#{type}" - defaults.compact! - defaults.flatten! - key = defaults.shift defaults = options.delete(:message) if options[:message] value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil) @@ -467,12 +470,30 @@ module ActiveModel default: defaults, model: @base.model_name.human, attribute: @base.class.human_attribute_name(attribute), - value: value + value: value, + object: @base }.merge!(options) I18n.translate(key, options) end + def marshal_dump # :nodoc: + [@base, without_default_proc(@messages), without_default_proc(@details)] + end + + def marshal_load(array) # :nodoc: + @base, @messages, @details = array + apply_default_array(@messages) + apply_default_array(@details) + end + + def init_with(coder) # :nodoc: + coder.map.each { |k, v| instance_variable_set(:"@#{k}", v) } + @details ||= {} + apply_default_array(@messages) + apply_default_array(@details) + end + private def normalize_message(attribute, message, options) case message @@ -483,9 +504,20 @@ module ActiveModel end end - def normalize_detail(attribute, message, options) + def normalize_detail(message, options) { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)) end + + def without_default_proc(hash) + hash.dup.tap do |new_h| + new_h.default_proc = nil + end + end + + def apply_default_array(hash) + hash.default_proc = proc { |h, key| h[key] = [] } + hash + end end # Raised when a validation cannot be corrected by end users and are considered @@ -506,7 +538,20 @@ module ActiveModel class StrictValidationFailed < StandardError end + # Raised when attribute values are out of range. + class RangeError < ::RangeError + end + # Raised when unknown attributes are supplied via mass assignment. + # + # class Person + # include ActiveModel::AttributeAssignment + # include ActiveModel::Validations + # end + # + # person = Person.new + # person.assign_attributes(name: 'Gorby') + # # => ActiveModel::UnknownAttributeError: unknown attribute 'name' for Person. class UnknownAttributeError < NoMethodError attr_reader :record, :attribute |