diff options
Diffstat (limited to 'activemodel')
| -rw-r--r-- | activemodel/lib/active_model/error.rb | 81 | ||||
| -rw-r--r-- | activemodel/lib/active_model/errors.rb | 357 | ||||
| -rw-r--r-- | activemodel/lib/active_model/lint.rb | 2 | ||||
| -rw-r--r-- | activemodel/lib/active_model/nested_error.rb | 33 | ||||
| -rw-r--r-- | activemodel/test/cases/error_test.rb | 200 | ||||
| -rw-r--r-- | activemodel/test/cases/errors_test.rb | 292 | ||||
| -rw-r--r-- | activemodel/test/cases/nested_error_test.rb | 54 | ||||
| -rw-r--r-- | activemodel/test/cases/validations/i18n_validation_test.rb | 19 | ||||
| -rw-r--r-- | activemodel/test/cases/validations/validations_context_test.rb | 4 | ||||
| -rw-r--r-- | activemodel/test/cases/validations/with_validation_test.rb | 8 | ||||
| -rw-r--r-- | activemodel/test/cases/validations_test.rb | 14 | ||||
| -rw-r--r-- | activemodel/test/models/person_with_validator.rb | 2 | ||||
| -rw-r--r-- | activemodel/test/models/reply.rb | 10 | ||||
| -rw-r--r-- | activemodel/test/validators/email_validator.rb | 5 | 
14 files changed, 899 insertions, 182 deletions
diff --git a/activemodel/lib/active_model/error.rb b/activemodel/lib/active_model/error.rb new file mode 100644 index 0000000000..5a1298e27f --- /dev/null +++ b/activemodel/lib/active_model/error.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module ActiveModel +  # == Active \Model \Error +  # +  # Represents one single error +  class Error +    CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict] +    MESSAGE_OPTIONS = [:message] + +    def initialize(base, attribute, type = :invalid, **options) +      @base = base +      @attribute = attribute +      @raw_type = type +      @type = type || :invalid +      @options = options +    end + +    def initialize_dup(other) +      @attribute = @attribute.dup +      @raw_type = @raw_type.dup +      @type = @type.dup +      @options = @options.deep_dup +    end + +    attr_reader :base, :attribute, :type, :raw_type, :options + +    def message +      case raw_type +      when Symbol +        base.errors.generate_message(attribute, raw_type, options.except(*CALLBACKS_OPTIONS)) +      else +        raw_type +      end +    end + +    def detail +      { error: raw_type }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)) +    end + +    def full_message +      base.errors.full_message(attribute, message) +    end + +    # See if error matches provided +attribute+, +type+ and +options+. +    def match?(attribute, type = nil, **options) +      if @attribute != attribute || (type && @type != type) +        return false +      end + +      options.each do |key, value| +        if @options[key] != value +          return false +        end +      end + +      true +    end + +    def strict_match?(attribute, type, **options) +      return false unless match?(attribute, type, **options) + +      full_message == Error.new(@base, attribute, type, **options).full_message +    end + +    def ==(other) +      other.is_a?(self.class) && attributes_for_hash == other.attributes_for_hash +    end +    alias eql? == + +    def hash +      attributes_for_hash.hash +    end + +    protected + +      def attributes_for_hash +        [@base, @attribute, @raw_type, @options] +      end +  end +end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 3a692a3e64..a9af426fb1 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -4,6 +4,10 @@ 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/deprecation" +require "active_model/error" +require "active_model/nested_error" +require "forwardable"  module ActiveModel    # == Active \Model \Errors @@ -59,15 +63,20 @@ module ActiveModel    class Errors      include Enumerable -    CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict] -    MESSAGE_OPTIONS = [:message] +    extend Forwardable +    def_delegators :@errors, :size, :clear, :blank?, :empty?, :uniq! +    # TODO: forward all enumerable methods after `each` deprecation is removed. +    def_delegators :@errors, :count + +    LEGACY_ATTRIBUTES = [:messages, :details].freeze      class << self        attr_accessor :i18n_customize_full_message # :nodoc:      end      self.i18n_customize_full_message = false -    attr_reader :messages, :details +    attr_reader :errors +    alias :objects :errors      # Pass in the instance of the object that is using the errors object.      # @@ -77,18 +86,17 @@ module ActiveModel      #     end      #   end      def initialize(base) -      @base     = base -      @messages = apply_default_array({}) -      @details = apply_default_array({}) +      @base = base +      @errors = []      end      def initialize_dup(other) # :nodoc: -      @messages = other.messages.dup -      @details  = other.details.deep_dup +      @errors = other.errors.deep_dup        super      end      # Copies the errors from <tt>other</tt>. +    # For copying errors but keep <tt>@base</tt> as is.      #      # other - The ActiveModel::Errors instance.      # @@ -96,11 +104,31 @@ module ActiveModel      #      #   person.errors.copy!(other)      def copy!(other) # :nodoc: -      @messages = other.messages.dup -      @details  = other.details.dup +      @errors = other.errors.deep_dup +      @errors.each { |error| +        error.instance_variable_set("@base", @base) +      } +    end + +    # Imports one error +    # Imported errors are wrapped as a NestedError, +    # providing access to original error object. +    # If attribute or type needs to be overriden, use `override_options`. +    # +    # override_options - Hash +    # @option override_options [Symbol] :attribute Override the attribute the error belongs to +    # @option override_options [Symbol] :type Override type of the error. +    def import(error, override_options = {}) +      [:attribute, :type].each do |key| +        if override_options.key?(key) +          override_options[key] = override_options[key].to_sym +        end +      end +      @errors.append(NestedError.new(@base, error, override_options))      end -    # Merges the errors from <tt>other</tt>. +    # Merges the errors from <tt>other</tt>, +    # each <tt>Error</tt> wrapped as <tt>NestedError</tt>.      #      # other - The ActiveModel::Errors instance.      # @@ -108,8 +136,9 @@ module ActiveModel      #      #   person.errors.merge!(other)      def merge!(other) -      @messages.merge!(other.messages) { |_, ary1, ary2| ary1 + ary2 } -      @details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 } +      other.errors.each { |error| +        import(error) +      }      end      # Removes all errors except the given keys. Returns a hash containing the removed errors. @@ -118,19 +147,31 @@ module ActiveModel      #   person.errors.slice!(:age, :gender) # => { :name=>["cannot be nil"], :city=>["cannot be nil"] }      #   person.errors.keys                  # => [:age, :gender]      def slice!(*keys) +      deprecation_removal_warning(:slice!) +        keys = keys.map(&:to_sym) -      @details.slice!(*keys) -      @messages.slice!(*keys) + +      results = messages.dup.slice!(*keys) + +      @errors.keep_if do |error| +        keys.include?(error.attribute) +      end + +      results      end -    # Clear the error messages. +    # Search for errors matching +attribute+, +type+ or +options+. +    # +    # Only supplied params will be matched.      # -    #   person.errors.full_messages # => ["name cannot be nil"] -    #   person.errors.clear -    #   person.errors.full_messages # => [] -    def clear -      messages.clear -      details.clear +    #   person.errors.where(:name) # => all name errors. +    #   person.errors.where(:name, :too_short) # => all name errors being too short +    #   person.errors.where(:name, :too_short, minimum: 2) # => all name errors being too short and minimum is 2 +    def where(attribute, type = nil, **options) +      attribute, type, options = normalize_arguments(attribute, type, options) +      @errors.select { |error| +        error.match?(attribute, type, options) +      }      end      # Returns +true+ if the error messages include an error for the given key @@ -140,8 +181,9 @@ module ActiveModel      #   person.errors.include?(:name) # => true      #   person.errors.include?(:age)  # => false      def include?(attribute) -      attribute = attribute.to_sym -      messages.key?(attribute) && messages[attribute].present? +      @errors.any? { |error| +        error.match?(attribute.to_sym) +      }      end      alias :has_key? :include?      alias :key? :include? @@ -151,10 +193,13 @@ module ActiveModel      #   person.errors[:name]        # => ["cannot be nil"]      #   person.errors.delete(:name) # => ["cannot be nil"]      #   person.errors[:name]        # => [] -    def delete(key) -      attribute = key.to_sym -      details.delete(attribute) -      messages.delete(attribute) +    def delete(attribute, type = nil, **options) +      attribute, type, options = normalize_arguments(attribute, type, options) +      matches = where(attribute, type, options) +      matches.each do |error| +        @errors.delete(error) +      end +      matches.map(&:message)      end      # When passed a symbol or a name of a method, returns an array of errors @@ -163,7 +208,7 @@ module ActiveModel      #   person.errors[:name]  # => ["cannot be nil"]      #   person.errors['name'] # => ["cannot be nil"]      def [](attribute) -      messages[attribute.to_sym] +      DeprecationHandlingMessageArray.new(messages_for(attribute), self, attribute)      end      # Iterates through each error key, value pair in the error messages hash. @@ -180,31 +225,37 @@ module ActiveModel      #     # Will yield :name and "can't be blank"      #     # then yield :name and "must be specified"      #   end -    def each -      messages.each_key do |attribute| -        messages[attribute].each { |error| yield attribute, error } -      end -    end +    def each(&block) +      if block.arity == 1 +        @errors.each(&block) +      else +        ActiveSupport::Deprecation.warn(<<-MSG.squish) +          Enumerating ActiveModel::Errors as a hash has been deprecated. +          In Rails 6, `errors` is an array of Error objects, +          therefore it should be accessed by a block with a single block +          parameter like this: + +          person.errors.each do |error| +            error.full_message +          end -    # Returns the number of error messages. -    # -    #   person.errors.add(:name, :blank, message: "can't be blank") -    #   person.errors.size # => 1 -    #   person.errors.add(:name, :not_specified, message: "must be specified") -    #   person.errors.size # => 2 -    def size -      values.flatten.size +          You are passing a block expecting 2 parameters, +          so the old hash behavior is simulated. As this is deprecated, +          this will result in an ArgumentError in Rails 6.1. +        MSG +        @errors. +          sort { |a, b| a.attribute <=> b.attribute }. +          each { |error| yield error.attribute, error.message } +      end      end -    alias :count :size      # Returns all message values.      #      #   person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}      #   person.errors.values   # => [["cannot be nil", "must be specified"]]      def values -      messages.select do |key, value| -        !value.empty? -      end.values +      deprecation_removal_warning(:values) +      @errors.map(&:message).freeze      end      # Returns all message keys. @@ -212,21 +263,12 @@ module ActiveModel      #   person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}      #   person.errors.keys     # => [:name]      def keys -      messages.select do |key, value| -        !value.empty? -      end.keys +      deprecation_removal_warning(:keys) +      keys = @errors.map(&:attribute) +      keys.uniq! +      keys.freeze      end -    # Returns +true+ if no errors are found, +false+ otherwise. -    # If the error message is a string it can be empty. -    # -    #   person.errors.full_messages # => ["name cannot be nil"] -    #   person.errors.empty?        # => false -    def empty? -      size.zero? -    end -    alias :blank? :empty? -      # Returns an xml formatted representation of the Errors hash.      #      #   person.errors.add(:name, :blank, message: "can't be blank") @@ -239,6 +281,7 @@ module ActiveModel      #   #    <error>name must be specified</error>      #   #  </errors>      def to_xml(options = {}) +      deprecation_removal_warning(:to_xml)        to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))      end @@ -258,13 +301,28 @@ module ActiveModel      #   person.errors.to_hash       # => {:name=>["cannot be nil"]}      #   person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}      def to_hash(full_messages = false) -      if full_messages -        messages.each_with_object({}) do |(attribute, array), messages| -          messages[attribute] = array.map { |message| full_message(attribute, message) } -        end -      else -        without_default_proc(messages) +      hash = {} +      message_method = full_messages ? :full_message : :message +      group_by_attribute.each do |attribute, errors| +        hash[attribute] = errors.map(&message_method)        end +      hash +    end + +    def messages +      DeprecationHandlingMessageHash.new(self) +    end + +    def details +      hash = {} +      group_by_attribute.each do |attribute, errors| +        hash[attribute] = errors.map(&:detail) +      end +      DeprecationHandlingDetailsHash.new(hash) +    end + +    def group_by_attribute +      @errors.group_by(&:attribute)      end      # Adds +message+ to the error messages and used validator type to +details+ on +attribute+. @@ -308,17 +366,20 @@ module ActiveModel      #   # => {:base=>["either name or email must be present"]}      #   person.errors.details      #   # => {:base=>[{error: :name_or_email_blank}]} -    def add(attribute, message = :invalid, options = {}) -      message = message.call if message.respond_to?(:call) -      detail  = normalize_detail(message, options) -      message = normalize_message(attribute, message, options) +    def add(attribute, type = :invalid, **options) +      error = Error.new( +        @base, +        *normalize_arguments(attribute, type, options) +      ) +        if exception = options[:strict]          exception = ActiveModel::StrictValidationFailed if exception == true -        raise exception, full_message(attribute, message) +        raise exception, error.full_message        end -      details[attribute.to_sym]  << detail -      messages[attribute.to_sym] << message +      @errors.append(error) + +      error      end      # Returns +true+ if an error on the attribute with the given message is @@ -337,13 +398,15 @@ module ActiveModel      #   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) +    def added?(attribute, type = :invalid, options = {}) +      attribute, type, options = normalize_arguments(attribute, type, options) -      if message.is_a? Symbol -        details[attribute.to_sym].include? normalize_detail(message, options) +      if type.is_a? Symbol +        @errors.any? { |error| +          error.strict_match?(attribute, type, options) +        }        else -        self[attribute].include? message +        messages_for(attribute).include?(type)        end      end @@ -359,12 +422,12 @@ module ActiveModel      #   person.errors.of_kind? :name, :not_too_long                            # => false      #   person.errors.of_kind? :name, "is too long"                            # => false      def of_kind?(attribute, message = :invalid) -      message = message.call if message.respond_to?(:call) +      attribute, message = normalize_arguments(attribute, message)        if message.is_a? Symbol -        details[attribute.to_sym].map { |e| e[:error] }.include? message +        !where(attribute, message).empty?        else -        self[attribute].include? message +        messages_for(attribute).include?(message)        end      end @@ -379,7 +442,7 @@ module ActiveModel      #   person.errors.full_messages      #   # => ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"]      def full_messages -      map { |attribute, message| full_message(attribute, message) } +      @errors.map(&:full_message)      end      alias :to_a :full_messages @@ -394,21 +457,16 @@ 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) } +      where(attribute).map(&:full_message).freeze +    end + +    def messages_for(attribute) +      where(attribute).map(&: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        attribute = attribute.to_s @@ -514,46 +572,111 @@ module ActiveModel        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) +      # Rails 5 +      @errors = [] +      @base = array[0] +      add_from_legacy_details_hash(array[2])      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) +      data = coder.map + +      data.each { |k, v| +        next if LEGACY_ATTRIBUTES.include?(k.to_sym) +        instance_variable_set(:"@#{k}", v) +      } + +      @errors ||= [] + +      # Legacy support Rails 5.x details hash +      add_from_legacy_details_hash(data["details"]) if data.key?("details")      end -  private -    def normalize_message(attribute, message, options) -      case message -      when Symbol -        generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS)) -      else -        message +    private + +      def normalize_arguments(attribute, type, **options) +        # Evaluate proc first +        if type.respond_to?(:call) +          type = type.call(@base, options) +        end + +        [attribute.to_sym, type, options] +      end + +      def add_from_legacy_details_hash(details) +        details.each { |attribute, errors| +          errors.each { |error| +            type = error.delete(:error) +            add(attribute, type, error) +          } +        } +      end + +      def deprecation_removal_warning(method_name) +        ActiveSupport::Deprecation.warn("ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 6.1") +      end + +      def deprecation_rename_warning(old_method_name, new_method_name) +        ActiveSupport::Deprecation.warn("ActiveModel::Errors##{old_method_name} is deprecated. Please call ##{new_method_name} instead.")        end +  end + +  class DeprecationHandlingMessageHash < SimpleDelegator +    def initialize(errors) +      @errors = errors +      super(prepare_content)      end -    def normalize_detail(message, options) -      { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)) +    def []=(attribute, value) +      ActiveSupport::Deprecation.warn("Calling `[]=` to an ActiveModel::Errors is deprecated. Please call `ActiveModel::Errors#add` instead.") + +      @errors.delete(attribute) +      Array(value).each do |message| +        @errors.add(attribute, message) +      end + +      __setobj__ prepare_content      end -    def without_default_proc(hash) -      hash.dup.tap do |new_h| -        new_h.default_proc = nil +    private + +      def prepare_content +        content = @errors.to_hash +        content.each do |attribute, value| +          content[attribute] = DeprecationHandlingMessageArray.new(value, @errors, attribute) +        end +        content.default_proc = proc do |hash, attribute| +          hash = hash.dup +          hash[attribute] = DeprecationHandlingMessageArray.new([], @errors, attribute) +          __setobj__ hash.freeze +          hash[attribute] +        end +        content.freeze        end +  end + +  class DeprecationHandlingMessageArray < SimpleDelegator +    def initialize(content, errors, attribute) +      @errors = errors +      @attribute = attribute +      super(content.freeze)      end -    def apply_default_array(hash) -      hash.default_proc = proc { |h, key| h[key] = [] } -      hash +    def <<(message) +      ActiveSupport::Deprecation.warn("Calling `<<` to an ActiveModel::Errors message array in order to add an error is deprecated. Please call `ActiveModel::Errors#add` instead.") + +      @errors.add(@attribute, message) +      __setobj__ @errors.messages_for(@attribute) +      self +    end +  end + +  class DeprecationHandlingDetailsHash < SimpleDelegator +    def initialize(details) +      details.default = [] +      details.freeze +      super(details)      end    end diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index b7ceabb59a..f9bfed95f1 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -101,7 +101,7 @@ module ActiveModel        # locale. If no error is present, the method should return an empty array.        def test_errors_aref          assert_respond_to model, :errors -        assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array" +        assert_equal [], model.errors[:hello], "errors#[] should return an empty Array"        end        private diff --git a/activemodel/lib/active_model/nested_error.rb b/activemodel/lib/active_model/nested_error.rb new file mode 100644 index 0000000000..93348c7771 --- /dev/null +++ b/activemodel/lib/active_model/nested_error.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "active_model/error" +require "forwardable" + +module ActiveModel +  # Represents one single error +  # @!attribute [r] base +  #   @return [ActiveModel::Base] the object which the error belongs to +  # @!attribute [r] attribute +  #   @return [Symbol] attribute of the object which the error belongs to +  # @!attribute [r] type +  #   @return [Symbol] error's type +  # @!attribute [r] options +  #   @return [Hash] additional options +  # @!attribute [r] inner_error +  #   @return [Error] inner error +  class NestedError < Error +    def initialize(base, inner_error, override_options = {}) +      @base = base +      @inner_error = inner_error +      @attribute = override_options.fetch(:attribute) { inner_error.attribute } +      @type = override_options.fetch(:type) { inner_error.type } +      @raw_type = inner_error.raw_type +      @options = inner_error.options +    end + +    attr_reader :inner_error + +    extend Forwardable +    def_delegators :@inner_error, :message +  end +end diff --git a/activemodel/test/cases/error_test.rb b/activemodel/test/cases/error_test.rb new file mode 100644 index 0000000000..d74321fee5 --- /dev/null +++ b/activemodel/test/cases/error_test.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +require "cases/helper" +require "active_model/error" + +class ErrorTest < ActiveModel::TestCase +  class Person +    extend ActiveModel::Naming +    def initialize +      @errors = ActiveModel::Errors.new(self) +    end + +    attr_accessor :name, :age +    attr_reader   :errors + +    def read_attribute_for_validation(attr) +      send(attr) +    end + +    def self.human_attribute_name(attr, options = {}) +      attr +    end + +    def self.lookup_ancestors +      [self] +    end +  end + +  def test_initialize +    base = Person.new +    error = ActiveModel::Error.new(base, :name, :too_long, foo: :bar) +    assert_equal base, error.base +    assert_equal :name, error.attribute +    assert_equal :too_long, error.type +    assert_equal({ foo: :bar }, error.options) +  end + +  test "initialize without type" do +    error = ActiveModel::Error.new(Person.new, :name) +    assert_equal :invalid, error.type +    assert_equal({}, error.options) +  end + +  test "initialize without type but with options" do +    options = { message: "bar" } +    error = ActiveModel::Error.new(Person.new, :name, options) +    assert_equal(options, error.options) +  end + +  # match? + +  test "match? handles mixed condition" do +    subject = ActiveModel::Error.new(Person.new, :mineral, :not_enough, count: 2) +    assert_not subject.match?(:mineral, :too_coarse) +    assert subject.match?(:mineral, :not_enough) +    assert subject.match?(:mineral, :not_enough, count: 2) +    assert_not subject.match?(:mineral, :not_enough, count: 1) +  end + +  test "match? handles attribute match" do +    subject = ActiveModel::Error.new(Person.new, :mineral, :not_enough, count: 2) +    assert_not subject.match?(:foo) +    assert subject.match?(:mineral) +  end + +  test "match? handles error type match" do +    subject = ActiveModel::Error.new(Person.new, :mineral, :not_enough, count: 2) +    assert_not subject.match?(:mineral, :too_coarse) +    assert subject.match?(:mineral, :not_enough) +  end + +  test "match? handles extra options match" do +    subject = ActiveModel::Error.new(Person.new, :mineral, :not_enough, count: 2) +    assert_not subject.match?(:mineral, :not_enough, count: 1) +    assert subject.match?(:mineral, :not_enough, count: 2) +  end + +  # message + +  test "message with type as a symbol" do +    error = ActiveModel::Error.new(Person.new, :name, :blank) +    assert_equal "can't be blank", error.message +  end + +  test "message with custom interpolation" do +    subject = ActiveModel::Error.new(Person.new, :name, :inclusion, message: "custom message %{value}", value: "name") +    assert_equal "custom message name", subject.message +  end + +  test "message returns plural interpolation" do +    subject = ActiveModel::Error.new(Person.new, :name, :too_long, count: 10) +    assert_equal "is too long (maximum is 10 characters)", subject.message +  end + +  test "message returns singular interpolation" do +    subject = ActiveModel::Error.new(Person.new, :name, :too_long, count: 1) +    assert_equal "is too long (maximum is 1 character)", subject.message +  end + +  test "message returns count interpolation" do +    subject = ActiveModel::Error.new(Person.new, :name, :too_long, message: "custom message %{count}", count: 10) +    assert_equal "custom message 10", subject.message +  end + +  test "message handles lambda in messages and option values, and i18n interpolation" do +    subject = ActiveModel::Error.new(Person.new, :name, :invalid, +      foo: "foo", +      bar: "bar", +      baz: Proc.new { "baz" }, +      message: Proc.new { |model, options| +        "%{attribute} %{foo} #{options[:bar]} %{baz}" +      } +    ) +    assert_equal "name foo bar baz", subject.message +  end + +  test "generate_message works without i18n_scope" do +    person = Person.new +    error = ActiveModel::Error.new(person, :name, :blank) +    assert_not_respond_to Person, :i18n_scope +    assert_nothing_raised { +      error.message +    } +  end + +  test "message with type as custom message" do +    error = ActiveModel::Error.new(Person.new, :name, message: "cannot be blank") +    assert_equal "cannot be blank", error.message +  end + +  test "message with options[:message] as custom message" do +    error = ActiveModel::Error.new(Person.new, :name, :blank, message: "cannot be blank") +    assert_equal "cannot be blank", error.message +  end + +  test "message renders lazily using current locale" do +    error = nil + +    I18n.backend.store_translations(:pl, errors: { messages: { invalid: "jest nieprawidłowe" } }) + +    I18n.with_locale(:en) { error = ActiveModel::Error.new(Person.new, :name, :invalid) } +    I18n.with_locale(:pl) { +      assert_equal "jest nieprawidłowe", error.message +    } +  end + +  test "message uses current locale" do +    I18n.backend.store_translations(:en, errors: { messages: { inadequate: "Inadequate %{attribute} found!" } }) +    error = ActiveModel::Error.new(Person.new, :name, :inadequate) +    assert_equal "Inadequate name found!", error.message +  end + +  # full_message + +  test "full_message returns the given message when attribute is :base" do +    error = ActiveModel::Error.new(Person.new, :base, message: "press the button") +    assert_equal "press the button", error.full_message +  end + +  test "full_message returns the given message with the attribute name included" do +    error = ActiveModel::Error.new(Person.new, :name, :blank) +    assert_equal "name can't be blank", error.full_message +  end + +  test "full_message uses default format" do +    error = ActiveModel::Error.new(Person.new, :name, message: "can't be blank") + +    # Use a locale without errors.format +    I18n.with_locale(:unknown) { +      assert_equal "name can't be blank", error.full_message +    } +  end + +  test "equality by base attribute, type and options" do +    person = Person.new + +    e1 = ActiveModel::Error.new(person, :name, foo: :bar) +    e2 = ActiveModel::Error.new(person, :name, foo: :bar) +    e2.instance_variable_set(:@_humanized_attribute, "Name") + +    assert_equal(e1, e2) +  end + +  test "inequality" do +    person = Person.new +    error = ActiveModel::Error.new(person, :name, foo: :bar) + +    assert error != ActiveModel::Error.new(person, :name, foo: :baz) +    assert error != ActiveModel::Error.new(person, :name) +    assert error != ActiveModel::Error.new(person, :title, foo: :bar) +    assert error != ActiveModel::Error.new(Person.new, :name, foo: :bar) +  end + +  test "comparing against different class would not raise error" do +    person = Person.new +    error = ActiveModel::Error.new(person, :name, foo: :bar) + +    assert error != person +  end +end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 947f9bf99b..0837e9db96 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -10,7 +10,7 @@ class ErrorsTest < ActiveModel::TestCase        @errors = ActiveModel::Errors.new(self)      end -    attr_accessor :name, :age +    attr_accessor :name, :age, :gender, :city      attr_reader   :errors      def validate! @@ -31,48 +31,47 @@ class ErrorsTest < ActiveModel::TestCase    end    def test_delete -    errors = ActiveModel::Errors.new(self) -    errors[:foo] << "omg" -    errors.delete("foo") -    assert_empty errors[:foo] +    errors = ActiveModel::Errors.new(Person.new) +    errors.add(:name, :blank) +    errors.delete("name") +    assert_empty errors[:name]    end    def test_include? -    errors = ActiveModel::Errors.new(self) -    errors[:foo] << "omg" +    errors = ActiveModel::Errors.new(Person.new) +    assert_deprecated { errors[:foo] << "omg" }      assert_includes errors, :foo, "errors should include :foo"      assert_includes errors, "foo", "errors should include 'foo' as :foo"    end    def test_dup -    errors = ActiveModel::Errors.new(self) -    errors[:foo] << "bar" +    errors = ActiveModel::Errors.new(Person.new) +    errors.add(:name)      errors_dup = errors.dup -    errors_dup[:bar] << "omg" -    assert_not_same errors_dup.messages, errors.messages +    assert_not_same errors_dup.errors, errors.errors    end    def test_has_key? -    errors = ActiveModel::Errors.new(self) -    errors[:foo] << "omg" +    errors = ActiveModel::Errors.new(Person.new) +    errors.add(:foo, "omg")      assert_equal true, errors.has_key?(:foo), "errors should have key :foo"      assert_equal true, errors.has_key?("foo"), "errors should have key 'foo' as :foo"    end    def test_has_no_key -    errors = ActiveModel::Errors.new(self) +    errors = ActiveModel::Errors.new(Person.new)      assert_equal false, errors.has_key?(:name), "errors should not have key :name"    end    def test_key? -    errors = ActiveModel::Errors.new(self) -    errors[:foo] << "omg" +    errors = ActiveModel::Errors.new(Person.new) +    errors.add(:foo, "omg")      assert_equal true, errors.key?(:foo), "errors should have key :foo"      assert_equal true, errors.key?("foo"), "errors should have key 'foo' as :foo"    end    def test_no_key -    errors = ActiveModel::Errors.new(self) +    errors = ActiveModel::Errors.new(Person.new)      assert_equal false, errors.key?(:name), "errors should not have key :name"    end @@ -86,42 +85,58 @@ class ErrorsTest < ActiveModel::TestCase    end    test "error access is indifferent" do -    errors = ActiveModel::Errors.new(self) -    errors[:foo] << "omg" +    errors = ActiveModel::Errors.new(Person.new) +    errors.add(:name, "omg") -    assert_equal ["omg"], errors["foo"] +    assert_equal ["omg"], errors["name"]    end    test "values returns an array of messages" do +    errors = ActiveModel::Errors.new(Person.new) +    assert_deprecated { errors.messages[:foo] = "omg" } +    assert_deprecated { errors.messages[:baz] = "zomg" } + +    assert_deprecated do +      assert_equal ["omg", "zomg"], errors.values +    end +  end + +  test "[]= overrides values" do      errors = ActiveModel::Errors.new(self) -    errors.messages[:foo] = "omg" -    errors.messages[:baz] = "zomg" +    assert_deprecated { errors.messages[:foo] = "omg" } +    assert_deprecated { errors.messages[:foo] = "zomg" } -    assert_equal ["omg", "zomg"], errors.values +    assert_equal ["zomg"], errors[:foo]    end    test "values returns an empty array after try to get a message only" do -    errors = ActiveModel::Errors.new(self) +    errors = ActiveModel::Errors.new(Person.new)      errors.messages[:foo]      errors.messages[:baz] -    assert_equal [], errors.values +    assert_deprecated do +      assert_equal [], errors.values +    end    end    test "keys returns the error keys" do -    errors = ActiveModel::Errors.new(self) -    errors.messages[:foo] << "omg" -    errors.messages[:baz] << "zomg" +    errors = ActiveModel::Errors.new(Person.new) +    assert_deprecated { errors.messages[:foo] << "omg" } +    assert_deprecated { errors.messages[:baz] << "zomg" } -    assert_equal [:foo, :baz], errors.keys +    assert_deprecated do +      assert_equal [:foo, :baz], errors.keys +    end    end    test "keys returns an empty array after try to get a message only" do -    errors = ActiveModel::Errors.new(self) +    errors = ActiveModel::Errors.new(Person.new)      errors.messages[:foo]      errors.messages[:baz] -    assert_equal [], errors.keys +    assert_deprecated do +      assert_equal [], errors.keys +    end    end    test "detecting whether there are errors with empty?, blank?, include?" do @@ -146,32 +161,108 @@ class ErrorsTest < ActiveModel::TestCase      assert_equal ["cannot be nil"], person.errors[:name]    end -  test "add an error message on a specific attribute" do +  test "add an error message on a specific attribute (deprecated)" do      person = Person.new      person.errors.add(:name, "cannot be blank")      assert_equal ["cannot be blank"], person.errors[:name]    end -  test "add an error message on a specific attribute with a defined type" do +  test "add an error message on a specific attribute with a defined type (deprecated)" do      person = Person.new      person.errors.add(:name, :blank, message: "cannot be blank")      assert_equal ["cannot be blank"], person.errors[:name]    end -  test "add an error with a symbol" do +  test "add an error with a symbol (deprecated)" do      person = Person.new      person.errors.add(:name, :blank)      message = person.errors.generate_message(:name, :blank)      assert_equal [message], person.errors[:name]    end -  test "add an error with a proc" do +  test "add an error with a proc (deprecated)" do      person = Person.new      message = Proc.new { "cannot be blank" }      person.errors.add(:name, message)      assert_equal ["cannot be blank"], person.errors[:name]    end +  test "add creates an error object and returns it" do +    person = Person.new +    error = person.errors.add(:name, :blank) + +    assert_equal :name, error.attribute +    assert_equal :blank, error.type +    assert_equal error, person.errors.objects.first +  end + +  test "add, with type as symbol" do +    person = Person.new +    person.errors.add(:name, :blank) + +    assert_equal :blank, person.errors.objects.first.type +    assert_equal ["can't be blank"], person.errors[:name] +  end + +  test "add, with type as String" do +    msg = "custom msg" + +    person = Person.new +    person.errors.add(:name, msg) + +    assert_equal [msg], person.errors[:name] +  end + +  test "add, with type as nil" do +    person = Person.new +    person.errors.add(:name) + +    assert_equal :invalid, person.errors.objects.first.type +    assert_equal ["is invalid"], person.errors[:name] +  end + +  test "add, with type as Proc, which evaluates to String" do +    msg = "custom msg" +    type = Proc.new { msg } + +    person = Person.new +    person.errors.add(:name, type) + +    assert_equal [msg], person.errors[:name] +  end + +  test "add, type being Proc, which evaluates to Symbol" do +    type = Proc.new { :blank } + +    person = Person.new +    person.errors.add(:name, type) + +    assert_equal :blank, person.errors.objects.first.type +    assert_equal ["can't be blank"], person.errors[:name] +  end + +  test "initialize options[:message] as Proc, which evaluates to String" do +    msg = "custom msg" +    type = Proc.new { msg } + +    person = Person.new +    person.errors.add(:name, :blank, message: type) + +    assert_equal :blank, person.errors.objects.first.type +    assert_equal [msg], person.errors[:name] +  end + +  test "add, with options[:message] as Proc, which evaluates to String, where type is nil" do +    msg = "custom msg" +    type = Proc.new { msg } + +    person = Person.new +    person.errors.add(:name, message: type) + +    assert_equal :invalid, person.errors.objects.first.type +    assert_equal [msg], person.errors[:name] +  end +    test "added? detects indifferent if a specific error was added to the object" do      person = Person.new      person.errors.add(:name, "cannot be blank") @@ -437,6 +528,32 @@ class ErrorsTest < ActiveModel::TestCase      assert_equal({ name: [{ error: :invalid }] }, person.errors.details)    end +  test "details retains original type as error" do +    errors = ActiveModel::Errors.new(Person.new) +    errors.add(:name, "cannot be nil") +    errors.add("foo", "bar") +    errors.add(:baz, nil) +    errors.add(:age, :invalid, count: 3, message: "%{count} is too low") + +    assert_equal( +      { +        name: [{ error: "cannot be nil" }], +        foo: [{ error: "bar" }], +        baz: [{ error: nil }], +        age: [{ error: :invalid, count: 3 }] +      }, +      errors.details +    ) +  end + +  test "group_by_attribute" do +    person = Person.new +    error = person.errors.add(:name, :invalid, message: "is bad") +    hash = person.errors.group_by_attribute + +    assert_equal({ name: [error] }, hash) +  end +    test "dup duplicates details" do      errors = ActiveModel::Errors.new(Person.new)      errors.add(:name, :invalid) @@ -449,7 +566,7 @@ class ErrorsTest < ActiveModel::TestCase      errors = ActiveModel::Errors.new(Person.new)      errors.add(:name, :invalid)      errors.delete(:name) -    assert_empty errors.details[:name] +    assert_not errors.added?(:name)    end    test "delete returns the deleted messages" do @@ -467,7 +584,7 @@ class ErrorsTest < ActiveModel::TestCase      assert_empty person.errors.details    end -  test "copy errors" do +  test "copy errors (deprecated)" do      errors = ActiveModel::Errors.new(Person.new)      errors.add(:name, :invalid)      person = Person.new @@ -477,7 +594,25 @@ class ErrorsTest < ActiveModel::TestCase      assert_equal [:name], person.errors.details.keys    end -  test "merge errors" do +  test "details returns empty array when accessed with non-existent attribute" do +    errors = ActiveModel::Errors.new(Person.new) + +    assert_equal [], errors.details[:foo] +  end + +  test "copy errors" do +    errors = ActiveModel::Errors.new(Person.new) +    errors.add(:name, :invalid) +    person = Person.new +    person.errors.copy!(errors) + +    assert person.errors.added?(:name, :invalid) +    person.errors.each do |error| +      assert_same person, error.base +    end +  end + +  test "merge errors (deprecated)" do      errors = ActiveModel::Errors.new(Person.new)      errors.add(:name, :invalid) @@ -489,6 +624,18 @@ class ErrorsTest < ActiveModel::TestCase      assert_equal({ name: [{ error: :blank }, { error: :invalid }] }, person.errors.details)    end +  test "merge errors" do +    errors = ActiveModel::Errors.new(Person.new) +    errors.add(:name, :invalid) + +    person = Person.new +    person.errors.add(:name, :blank) +    person.errors.merge!(errors) + +    assert(person.errors.added?(:name, :invalid)) +    assert(person.errors.added?(:name, :blank)) +  end +    test "slice! removes all errors except the given keys" do      person = Person.new      person.errors.add(:name, "cannot be nil") @@ -496,9 +643,9 @@ class ErrorsTest < ActiveModel::TestCase      person.errors.add(:gender, "cannot be nil")      person.errors.add(:city, "cannot be nil") -    person.errors.slice!(:age, "gender") +    assert_deprecated { person.errors.slice!(:age, "gender") } -    assert_equal [:age, :gender], person.errors.keys +    assert_equal [:age, :gender], assert_deprecated { person.errors.keys }    end    test "slice! returns the deleted errors" do @@ -508,7 +655,7 @@ class ErrorsTest < ActiveModel::TestCase      person.errors.add(:gender, "cannot be nil")      person.errors.add(:city, "cannot be nil") -    removed_errors = person.errors.slice!(:age, "gender") +    removed_errors = assert_deprecated { person.errors.slice!(:age, "gender") }      assert_equal({ name: ["cannot be nil"], city: ["cannot be nil"] }, removed_errors)    end @@ -518,10 +665,23 @@ class ErrorsTest < ActiveModel::TestCase      errors.add(:name, :invalid)      serialized = Marshal.load(Marshal.dump(errors)) +    assert_equal Person, serialized.instance_variable_get(:@base).class      assert_equal errors.messages, serialized.messages      assert_equal errors.details, serialized.details    end +  test "errors are compatible with marshal dumped from Rails 5.x" do +    # Derived from +    # errors = ActiveModel::Errors.new(Person.new) +    # errors.add(:name, :invalid) +    dump = "\x04\bU:\x18ActiveModel::Errors[\bo:\x17ErrorsTest::Person\x06:\f@errorsU;\x00[\b@\a{\x00{\x00{\x06:\tname[\x06I\"\x0Fis invalid\x06:\x06ET{\x06;\b[\x06{\x06:\nerror:\finvalid" +    serialized = Marshal.load(dump) + +    assert_equal Person, serialized.instance_variable_get(:@base).class +    assert_equal({ name: ["is invalid"] }, serialized.messages) +    assert_equal({ name: [{ error: :invalid }] }, serialized.details) +  end +    test "errors are backward compatible with the Rails 4.2 format" do      yaml = <<~CODE      --- !ruby/object:ActiveModel::Errors @@ -541,4 +701,54 @@ class ErrorsTest < ActiveModel::TestCase      assert_equal({}, errors.messages)      assert_equal({}, errors.details)    end + +  test "errors are compatible with YAML dumped from Rails 5.x" do +    yaml = <<~CODE +    --- !ruby/object:ActiveModel::Errors +    base: &1 !ruby/object:ErrorsTest::Person +      errors: !ruby/object:ActiveModel::Errors +        base: *1 +        messages: {} +        details: {} +    messages: +      :name: +      - is invalid +    details: +      :name: +      - :error: :invalid +    CODE + +    errors = YAML.load(yaml) +    assert_equal({ name: ["is invalid"] }, errors.messages) +    assert_equal({ name: [{ error: :invalid }] }, errors.details) + +    errors.clear +    assert_equal({}, errors.messages) +    assert_equal({}, errors.details) +  end + +  test "errors are compatible with YAML dumped from Rails 6.x" do +    yaml = <<~CODE +    --- !ruby/object:ActiveModel::Errors +    base: &1 !ruby/object:ErrorsTest::Person +      errors: !ruby/object:ActiveModel::Errors +        base: *1 +        errors: [] +    errors: +    - !ruby/object:ActiveModel::Error +      base: *1 +      attribute: :name +      type: :invalid +      raw_type: :invalid +      options: {} +    CODE + +    errors = YAML.load(yaml) +    assert_equal({ name: ["is invalid"] }, errors.messages) +    assert_equal({ name: [{ error: :invalid }] }, errors.details) + +    errors.clear +    assert_equal({}, errors.messages) +    assert_equal({}, errors.details) +  end  end diff --git a/activemodel/test/cases/nested_error_test.rb b/activemodel/test/cases/nested_error_test.rb new file mode 100644 index 0000000000..5bad100da5 --- /dev/null +++ b/activemodel/test/cases/nested_error_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "cases/helper" +require "active_model/nested_error" +require "models/topic" +require "models/reply" + +class ErrorTest < ActiveModel::TestCase +  def test_initialize +    topic = Topic.new +    inner_error = ActiveModel::Error.new(topic, :title, :not_enough, count: 2) +    reply = Reply.new +    error = ActiveModel::NestedError.new(reply, inner_error) + +    assert_equal reply, error.base +    assert_equal inner_error.attribute, error.attribute +    assert_equal inner_error.type, error.type +    assert_equal(inner_error.options, error.options) +  end + +  test "initialize with overriding attribute and type" do +    topic = Topic.new +    inner_error = ActiveModel::Error.new(topic, :title, :not_enough, count: 2) +    reply = Reply.new +    error = ActiveModel::NestedError.new(reply, inner_error, attribute: :parent, type: :foo) + +    assert_equal reply, error.base +    assert_equal :parent, error.attribute +    assert_equal :foo, error.type +    assert_equal(inner_error.options, error.options) +  end + +  def test_message +    topic = Topic.new(author_name: "Bruce") +    inner_error = ActiveModel::Error.new(topic, :title, :not_enough, message: Proc.new { |model, options| +      "not good enough for #{model.author_name}" +    }) +    reply = Reply.new(author_name: "Mark") +    error = ActiveModel::NestedError.new(reply, inner_error) + +    assert_equal "not good enough for Bruce", error.message +  end + +  def test_full_message +    topic = Topic.new(author_name: "Bruce") +    inner_error = ActiveModel::Error.new(topic, :title, :not_enough, message: Proc.new { |model, options| +      "not good enough for #{model.author_name}" +    }) +    reply = Reply.new(author_name: "Mark") +    error = ActiveModel::NestedError.new(reply, inner_error) + +    assert_equal "Title not good enough for Bruce", error.full_message +  end +end diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index 35bb918f26..b7ee50832c 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -169,8 +169,8 @@ class I18nValidationTest < ActiveModel::TestCase      # [ case,                              validation_options,            generate_message_options]      [ "given no options",                  {},                            {}],      [ "given custom message",              { message: "custom" },         { message: "custom" }], -    [ "given if condition",                { if:                          lambda { true } },  {}], -    [ "given unless condition",            { unless:                      lambda { false } }, {}], +    [ "given if condition",                { if: lambda { true } },       {}], +    [ "given unless condition",            { unless: lambda { false } },  {}],      [ "given option that is not reserved", { format: "jpg" },             { format: "jpg" }]    ] @@ -181,6 +181,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title_confirmation, :confirmation, generate_message_options.merge(attribute: "Title")]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -191,6 +192,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :accepted, generate_message_options]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -201,6 +203,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :blank, generate_message_options]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -211,6 +214,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :too_short, generate_message_options.merge(count: 3)]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -222,6 +226,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :too_long, generate_message_options.merge(count: 5)]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -232,6 +237,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :wrong_length, generate_message_options.merge(count: 5)]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -243,6 +249,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :invalid, generate_message_options.merge(value: "72x")]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -254,6 +261,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :inclusion, generate_message_options.merge(value: "z")]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -265,6 +273,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :inclusion, generate_message_options.merge(value: "z")]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -276,6 +285,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :exclusion, generate_message_options.merge(value: "a")]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -287,6 +297,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :exclusion, generate_message_options.merge(value: "a")]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -298,6 +309,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :not_a_number, generate_message_options.merge(value: "a")]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -309,6 +321,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :not_an_integer, generate_message_options.merge(value: "0.0")]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -320,6 +333,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :odd, generate_message_options.merge(value: 0)]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end @@ -331,6 +345,7 @@ class I18nValidationTest < ActiveModel::TestCase        call = [:title, :less_than, generate_message_options.merge(value: 1, count: 0)]        assert_called_with(@person.errors, :generate_message, call) do          @person.valid? +        @person.errors.messages        end      end    end diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb index 024eb1882f..3d2dea9828 100644 --- a/activemodel/test/cases/validations/validations_context_test.rb +++ b/activemodel/test/cases/validations/validations_context_test.rb @@ -14,13 +14,13 @@ class ValidationsContextTest < ActiveModel::TestCase    class ValidatorThatAddsErrors < ActiveModel::Validator      def validate(record) -      record.errors[:base] << ERROR_MESSAGE +      record.errors.add(:base, ERROR_MESSAGE)      end    end    class AnotherValidatorThatAddsErrors < ActiveModel::Validator      def validate(record) -      record.errors[:base] << ANOTHER_ERROR_MESSAGE +      record.errors.add(:base, ANOTHER_ERROR_MESSAGE)      end    end diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 8239792c79..e6ae6603f2 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -14,13 +14,13 @@ class ValidatesWithTest < ActiveModel::TestCase    class ValidatorThatAddsErrors < ActiveModel::Validator      def validate(record) -      record.errors[:base] << ERROR_MESSAGE +      record.errors.add(:base, message: ERROR_MESSAGE)      end    end    class OtherValidatorThatAddsErrors < ActiveModel::Validator      def validate(record) -      record.errors[:base] << OTHER_ERROR_MESSAGE +      record.errors.add(:base, message: OTHER_ERROR_MESSAGE)      end    end @@ -32,14 +32,14 @@ class ValidatesWithTest < ActiveModel::TestCase    class ValidatorThatValidatesOptions < ActiveModel::Validator      def validate(record)        if options[:field] == :first_name -        record.errors[:base] << ERROR_MESSAGE +        record.errors.add(:base, message: ERROR_MESSAGE)        end      end    end    class ValidatorPerEachAttribute < ActiveModel::EachValidator      def validate_each(record, attribute, value) -      record.errors[attribute] << "Value is #{value}" +      record.errors.add(attribute, message: "Value is #{value}")      end    end diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 7776233db5..0b9e1b7005 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -53,7 +53,7 @@ class ValidationsTest < ActiveModel::TestCase      r = Reply.new      r.valid? -    errors = r.errors.collect { |attr, messages| [attr.to_s, messages] } +    errors = assert_deprecated { r.errors.collect { |attr, messages| [attr.to_s, messages] } }      assert_includes errors, ["title", "is Empty"]      assert_includes errors, ["content", "is Empty"] @@ -74,7 +74,7 @@ class ValidationsTest < ActiveModel::TestCase    def test_errors_on_nested_attributes_expands_name      t = Topic.new -    t.errors["replies.name"] << "can't be blank" +    assert_deprecated { t.errors["replies.name"] << "can't be blank" }      assert_equal ["Replies name can't be blank"], t.errors.full_messages    end @@ -216,7 +216,7 @@ class ValidationsTest < ActiveModel::TestCase      t = Topic.new      assert_predicate t, :invalid? -    xml = t.errors.to_xml +    xml = assert_deprecated { t.errors.to_xml }      assert_match %r{<errors>}, xml      assert_match %r{<error>Title can't be blank</error>}, xml      assert_match %r{<error>Content can't be blank</error>}, xml @@ -241,14 +241,14 @@ class ValidationsTest < ActiveModel::TestCase      t = Topic.new title: ""      assert_predicate t, :invalid? -    assert_equal :title, key = t.errors.keys[0] +    assert_equal :title, key = assert_deprecated { t.errors.keys[0] }      assert_equal "can't be blank", t.errors[key][0]      assert_equal "is too short (minimum is 2 characters)", t.errors[key][1] -    assert_equal :author_name, key = t.errors.keys[1] +    assert_equal :author_name, key = assert_deprecated { t.errors.keys[1] }      assert_equal "can't be blank", t.errors[key][0] -    assert_equal :author_email_address, key = t.errors.keys[2] +    assert_equal :author_email_address, key = assert_deprecated { t.errors.keys[2] }      assert_equal "will never be valid", t.errors[key][0] -    assert_equal :content, key = t.errors.keys[3] +    assert_equal :content, key = assert_deprecated { t.errors.keys[3] }      assert_equal "is too short (minimum is 2 characters)", t.errors[key][0]    end diff --git a/activemodel/test/models/person_with_validator.rb b/activemodel/test/models/person_with_validator.rb index 44e78cbc29..fbb28d2a0f 100644 --- a/activemodel/test/models/person_with_validator.rb +++ b/activemodel/test/models/person_with_validator.rb @@ -5,7 +5,7 @@ class PersonWithValidator    class PresenceValidator < ActiveModel::EachValidator      def validate_each(record, attribute, value) -      record.errors[attribute] << "Local validator#{options[:custom]}" if value.blank? +      record.errors.add(attribute, message: "Local validator#{options[:custom]}") if value.blank?      end    end diff --git a/activemodel/test/models/reply.rb b/activemodel/test/models/reply.rb index 6bb18f95fe..f340b6fb14 100644 --- a/activemodel/test/models/reply.rb +++ b/activemodel/test/models/reply.rb @@ -11,24 +11,24 @@ class Reply < Topic    validate :check_wrong_update,     on: :update    def check_empty_title -    errors[:title] << "is Empty" unless title && title.size > 0 +    errors.add(:title, "is Empty") unless title && title.size > 0    end    def errors_on_empty_content -    errors[:content] << "is Empty" unless content && content.size > 0 +    errors.add(:content, "is Empty") unless content && content.size > 0    end    def check_content_mismatch      if title && content && content == "Mismatch" -      errors[:title] << "is Content Mismatch" +      errors.add(:title, "is Content Mismatch")      end    end    def title_is_wrong_create -    errors[:title] << "is Wrong Create" if title && title == "Wrong Create" +    errors.add(:title, "is Wrong Create") if title && title == "Wrong Create"    end    def check_wrong_update -    errors[:title] << "is Wrong Update" if title && title == "Wrong Update" +    errors.add(:title, "is Wrong Update") if title && title == "Wrong Update"    end  end diff --git a/activemodel/test/validators/email_validator.rb b/activemodel/test/validators/email_validator.rb index 0c634d8659..774a10b2ba 100644 --- a/activemodel/test/validators/email_validator.rb +++ b/activemodel/test/validators/email_validator.rb @@ -2,7 +2,8 @@  class EmailValidator < ActiveModel::EachValidator    def validate_each(record, attribute, value) -    record.errors[attribute] << (options[:message] || "is not an email") unless -      /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value) +    unless /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value) +      record.errors.add(attribute, message: options[:message] || "is not an email") +    end    end  end  | 
