aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel/lib')
-rw-r--r--activemodel/lib/active_model.rb3
-rw-r--r--activemodel/lib/active_model/attribute_assignment.rb1
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb33
-rw-r--r--activemodel/lib/active_model/attribute_mutation_tracker.rb117
-rw-r--r--activemodel/lib/active_model/attribute_set.rb14
-rw-r--r--activemodel/lib/active_model/attributes.rb63
-rw-r--r--activemodel/lib/active_model/callbacks.rb2
-rw-r--r--activemodel/lib/active_model/dirty.rb135
-rw-r--r--activemodel/lib/active_model/error.rb165
-rw-r--r--activemodel/lib/active_model/errors.rb469
-rw-r--r--activemodel/lib/active_model/gem_version.rb2
-rw-r--r--activemodel/lib/active_model/lint.rb2
-rw-r--r--activemodel/lib/active_model/naming.rb1
-rw-r--r--activemodel/lib/active_model/nested_error.rb33
-rw-r--r--activemodel/lib/active_model/railtie.rb4
-rw-r--r--activemodel/lib/active_model/secure_password.rb36
-rw-r--r--activemodel/lib/active_model/serialization.rb1
-rw-r--r--activemodel/lib/active_model/serializers/json.rb7
-rw-r--r--activemodel/lib/active_model/type/big_integer.rb1
-rw-r--r--activemodel/lib/active_model/type/boolean.rb12
-rw-r--r--activemodel/lib/active_model/type/date.rb8
-rw-r--r--activemodel/lib/active_model/type/date_time.rb6
-rw-r--r--activemodel/lib/active_model/type/decimal.rb5
-rw-r--r--activemodel/lib/active_model/type/float.rb3
-rw-r--r--activemodel/lib/active_model/type/helpers.rb1
-rw-r--r--activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb4
-rw-r--r--activemodel/lib/active_model/type/helpers/numeric.rb12
-rw-r--r--activemodel/lib/active_model/type/helpers/time_value.rb32
-rw-r--r--activemodel/lib/active_model/type/helpers/timezone.rb19
-rw-r--r--activemodel/lib/active_model/type/immutable_string.rb1
-rw-r--r--activemodel/lib/active_model/type/integer.rb19
-rw-r--r--activemodel/lib/active_model/type/string.rb1
-rw-r--r--activemodel/lib/active_model/type/time.rb4
-rw-r--r--activemodel/lib/active_model/type/value.rb3
-rw-r--r--activemodel/lib/active_model/validations.rb3
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb6
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb1
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb1
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb4
-rw-r--r--activemodel/lib/active_model/validations/format.rb1
-rw-r--r--activemodel/lib/active_model/validations/length.rb2
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb34
-rw-r--r--activemodel/lib/active_model/validations/validates.rb1
-rw-r--r--activemodel/lib/active_model/validator.rb3
44 files changed, 803 insertions, 472 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index bc10d6b4b9..756473e38d 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2004-2018 David Heinemeier Hansson
+# Copyright (c) 2004-2019 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -53,6 +53,7 @@ module ActiveModel
eager_autoload do
autoload :Errors
+ autoload :Error
autoload :RangeError, "active_model/errors"
autoload :StrictValidationFailed, "active_model/errors"
autoload :UnknownAttributeError, "active_model/errors"
diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb
index f0e3458f51..9bdec0dfda 100644
--- a/activemodel/lib/active_model/attribute_assignment.rb
+++ b/activemodel/lib/active_model/attribute_assignment.rb
@@ -38,7 +38,6 @@ module ActiveModel
alias attributes= assign_attributes
private
-
def _assign_attributes(attributes)
attributes.each do |k, v|
_assign_attribute(k, v)
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index d8352343e9..415f1f679b 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -286,12 +286,12 @@ module ActiveModel
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
- generate_method = "define_method_#{matcher.method_missing_target}"
+ generate_method = "define_method_#{matcher.target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
- define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s
+ define_proxy_call true, generated_attribute_methods, method_name, matcher.target, attr_name.to_s
end
end
end
@@ -352,17 +352,18 @@ module ActiveModel
def attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
- # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix
- # will match every time.
+ # Bump plain matcher to last place so that only methods that do not
+ # match any other pattern match the actual attribute name.
+ # This is currently only needed to support legacy usage.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
- matchers.map { |method| method.match(method_name) }.compact
+ matchers.map { |matcher| matcher.match(method_name) }.compact
end
end
# Define a method `name` in `mod` that dispatches to `send`
# using the given `extra` args. This falls back on `define_method`
# and `send` if the given names cannot be compiled.
- def define_proxy_call(include_private, mod, name, send, *extra)
+ def define_proxy_call(include_private, mod, name, target, *extra)
defn = if NAME_COMPILABLE_REGEXP.match?(name)
"def #{name}(*args)"
else
@@ -371,34 +372,34 @@ module ActiveModel
extra = (extra.map!(&:inspect) << "*args").join(", ")
- target = if CALL_COMPILABLE_REGEXP.match?(send)
- "#{"self." unless include_private}#{send}(#{extra})"
+ body = if CALL_COMPILABLE_REGEXP.match?(target)
+ "#{"self." unless include_private}#{target}(#{extra})"
else
- "send(:'#{send}', #{extra})"
+ "send(:'#{target}', #{extra})"
end
mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
#{defn}
- #{target}
+ #{body}
end
RUBY
end
class AttributeMethodMatcher #:nodoc:
- attr_reader :prefix, :suffix, :method_missing_target
+ attr_reader :prefix, :suffix, :target
- AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
+ AttributeMethodMatch = Struct.new(:target, :attr_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
- @method_missing_target = "#{@prefix}attribute#{@suffix}"
+ @target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
- AttributeMethodMatch.new(method_missing_target, $1, method_name)
+ AttributeMethodMatch.new(target, $1)
end
end
@@ -507,8 +508,8 @@ module ActiveModel
temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
yield temp_method_name, attr_name_expr
- mod.send(:alias_method, method_name, temp_method_name)
- mod.send(:undef_method, temp_method_name)
+ mod.alias_method method_name, temp_method_name
+ mod.undef_method temp_method_name
end
end
end
diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb
index 6abf37bd44..d8cd48a53b 100644
--- a/activemodel/lib/active_model/attribute_mutation_tracker.rb
+++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb
@@ -1,14 +1,15 @@
# frozen_string_literal: true
require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/object/duplicable"
module ActiveModel
class AttributeMutationTracker # :nodoc:
OPTION_NOT_GIVEN = Object.new
- def initialize(attributes)
+ def initialize(attributes, forced_changes = Set.new)
@attributes = attributes
- @forced_changes = Set.new
+ @forced_changes = forced_changes
end
def changed_attribute_names
@@ -18,24 +19,22 @@ module ActiveModel
def changed_values
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
if changed?(attr_name)
- result[attr_name] = attributes[attr_name].original_value
+ result[attr_name] = original_value(attr_name)
end
end
end
def changes
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
- change = change_to_attribute(attr_name)
- if change
+ if change = change_to_attribute(attr_name)
result.merge!(attr_name => change)
end
end
end
def change_to_attribute(attr_name)
- attr_name = attr_name.to_s
if changed?(attr_name)
- [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
+ [original_value(attr_name), fetch_value(attr_name)]
end
end
@@ -44,29 +43,26 @@ module ActiveModel
end
def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
- attr_name = attr_name.to_s
- forced_changes.include?(attr_name) ||
- attributes[attr_name].changed? &&
- (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
- (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
+ attribute_changed?(attr_name) &&
+ (OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
+ (OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
end
def changed_in_place?(attr_name)
- attributes[attr_name.to_s].changed_in_place?
+ attributes[attr_name].changed_in_place?
end
def forget_change(attr_name)
- attr_name = attr_name.to_s
attributes[attr_name] = attributes[attr_name].forgetting_assignment
forced_changes.delete(attr_name)
end
def original_value(attr_name)
- attributes[attr_name.to_s].original_value
+ attributes[attr_name].original_value
end
def force_change(attr_name)
- forced_changes << attr_name.to_s
+ forced_changes << attr_name
end
private
@@ -75,45 +71,108 @@ module ActiveModel
def attr_names
attributes.keys
end
+
+ def attribute_changed?(attr_name)
+ forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
+ end
+
+ def fetch_value(attr_name)
+ attributes.fetch_value(attr_name)
+ end
+ end
+
+ class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
+ def initialize(attributes, forced_changes = {})
+ super
+ @finalized_changes = nil
+ end
+
+ def changed_in_place?(attr_name)
+ false
+ end
+
+ def change_to_attribute(attr_name)
+ if finalized_changes&.include?(attr_name)
+ finalized_changes[attr_name].dup
+ else
+ super
+ end
+ end
+
+ def forget_change(attr_name)
+ forced_changes.delete(attr_name)
+ end
+
+ def original_value(attr_name)
+ if changed?(attr_name)
+ forced_changes[attr_name]
+ else
+ fetch_value(attr_name)
+ end
+ end
+
+ def force_change(attr_name)
+ forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
+ end
+
+ def finalize_changes
+ @finalized_changes = changes
+ end
+
+ private
+ attr_reader :finalized_changes
+
+ def attr_names
+ forced_changes.keys
+ end
+
+ def attribute_changed?(attr_name)
+ forced_changes.include?(attr_name)
+ end
+
+ def fetch_value(attr_name)
+ attributes.send(:_read_attribute, attr_name)
+ end
+
+ def clone_value(attr_name)
+ value = fetch_value(attr_name)
+ value.duplicable? ? value.clone : value
+ rescue TypeError, NoMethodError
+ value
+ end
end
class NullMutationTracker # :nodoc:
include Singleton
- def changed_attribute_names(*)
+ def changed_attribute_names
[]
end
- def changed_values(*)
+ def changed_values
{}
end
- def changes(*)
+ def changes
{}
end
def change_to_attribute(attr_name)
end
- def any_changes?(*)
+ def any_changes?
false
end
- def changed?(*)
+ def changed?(attr_name, **)
false
end
- def changed_in_place?(*)
+ def changed_in_place?(attr_name)
false
end
- def forget_change(*)
- end
-
- def original_value(*)
- end
-
- def force_change(*)
+ def original_value(attr_name)
end
end
end
diff --git a/activemodel/lib/active_model/attribute_set.rb b/activemodel/lib/active_model/attribute_set.rb
index a890ee3932..2f5437ceda 100644
--- a/activemodel/lib/active_model/attribute_set.rb
+++ b/activemodel/lib/active_model/attribute_set.rb
@@ -37,16 +37,8 @@ module ActiveModel
attributes.each_key.select { |name| self[name].initialized? }
end
- if defined?(JRUBY_VERSION)
- # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
- # https://github.com/jruby/jruby/pull/2562
- def fetch_value(name, &block)
- self[name].value(&block)
- end
- else
- def fetch_value(name)
- self[name].value { |n| yield n if block_given? }
- end
+ def fetch_value(name, &block)
+ self[name].value(&block)
end
def write_from_database(name, value)
@@ -102,11 +94,9 @@ module ActiveModel
end
protected
-
attr_reader :attributes
private
-
def initialized_attributes
attributes.select { |_, attr| attr.initialized? }
end
diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb
index c3a446098c..c586f52b78 100644
--- a/activemodel/lib/active_model/attributes.rb
+++ b/activemodel/lib/active_model/attributes.rb
@@ -26,8 +26,22 @@ module ActiveModel
define_attribute_method(name)
end
- private
+ # Returns an array of attribute names as strings
+ #
+ # class Person
+ # include ActiveModel::Attributes
+ #
+ # attribute :name, :string
+ # attribute :age, :integer
+ # end
+ #
+ # Person.attribute_names
+ # # => ["name", "age"]
+ def attribute_names
+ attribute_types.keys
+ end
+ private
def define_method_attribute=(name)
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name, writer: true,
@@ -65,33 +79,56 @@ module ActiveModel
super
end
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
+ #
+ # class Person
+ # include ActiveModel::Model
+ # include ActiveModel::Attributes
+ #
+ # attribute :name, :string
+ # attribute :age, :integer
+ # end
+ #
+ # person = Person.new(name: 'Francesco', age: 22)
+ # person.attributes
+ # # => {"name"=>"Francesco", "age"=>22}
def attributes
@attributes.to_hash
end
- private
+ # Returns an array of attribute names as strings
+ #
+ # class Person
+ # include ActiveModel::Attributes
+ #
+ # attribute :name, :string
+ # attribute :age, :integer
+ # end
+ #
+ # person = Person.new
+ # person.attribute_names
+ # # => ["name", "age"]
+ def attribute_names
+ @attributes.keys
+ end
+ private
def write_attribute(attr_name, value)
- name = if self.class.attribute_alias?(attr_name)
- self.class.attribute_alias(attr_name).to_s
- else
- attr_name.to_s
- end
+ name = attr_name.to_s
+ name = self.class.attribute_aliases[name] || name
@attributes.write_from_user(name, value)
value
end
def attribute(attr_name)
- name = if self.class.attribute_alias?(attr_name)
- self.class.attribute_alias(attr_name).to_s
- else
- attr_name.to_s
- end
+ name = attr_name.to_s
+ name = self.class.attribute_aliases[name] || name
+
@attributes.fetch_value(name)
end
- # Handle *= for method_missing.
+ # Dispatch target for <tt>*=</tt> attribute methods.
def attribute=(attribute_name, value)
write_attribute(attribute_name, value)
end
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
index fde3381df2..7a544395cb 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/hash/keys"
module ActiveModel
# == Active \Model \Callbacks
@@ -125,7 +126,6 @@ module ActiveModel
end
private
-
def _define_before_model_callback(klass, callback)
klass.define_singleton_method("before_#{callback}") do |*args, **options, &block|
options.assert_valid_keys(:if, :unless, :prepend)
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 0d9e761b1e..aaefe00c83 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/hash_with_indifferent_access"
-require "active_support/core_ext/object/duplicable"
require "active_model/attribute_mutation_tracker"
module ActiveModel
@@ -122,9 +120,6 @@ module ActiveModel
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
- OPTION_NOT_GIVEN = Object.new # :nodoc:
- private_constant :OPTION_NOT_GIVEN
-
included do
attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
attribute_method_suffix "_previously_changed?", "_previous_change"
@@ -141,14 +136,13 @@ module ActiveModel
@mutations_from_database = nil
end
- # Clears dirty data and moves +changes+ to +previously_changed+ and
+ # Clears dirty data and moves +changes+ to +previous_changes+ and
# +mutations_from_database+ to +mutations_before_last_save+ respectively.
def changes_applied
unless defined?(@attributes)
- @previously_changed = changes
+ mutations_from_database.finalize_changes
end
@mutations_before_last_save = mutations_from_database
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
forget_attribute_assignments
@mutations_from_database = nil
end
@@ -159,7 +153,7 @@ module ActiveModel
# person.name = 'bob'
# person.changed? # => true
def changed?
- changed_attributes.present?
+ mutations_from_database.any_changes?
end
# Returns an array with the name of the attributes with unsaved changes.
@@ -168,42 +162,37 @@ module ActiveModel
# person.name = 'bob'
# person.changed # => ["name"]
def changed
- changed_attributes.keys
+ mutations_from_database.changed_attribute_names
end
- # Handles <tt>*_changed?</tt> for +method_missing+.
- def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
- !!changes_include?(attr) &&
- (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
- (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
+ # Dispatch target for <tt>*_changed?</tt> attribute methods.
+ def attribute_changed?(attr_name, **options) # :nodoc:
+ mutations_from_database.changed?(attr_name.to_s, options)
end
- # Handles <tt>*_was</tt> for +method_missing+.
- def attribute_was(attr) # :nodoc:
- attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
+ # Dispatch target for <tt>*_was</tt> attribute methods.
+ def attribute_was(attr_name) # :nodoc:
+ mutations_from_database.original_value(attr_name.to_s)
end
- # Handles <tt>*_previously_changed?</tt> for +method_missing+.
- def attribute_previously_changed?(attr) #:nodoc:
- previous_changes_include?(attr)
+ # Dispatch target for <tt>*_previously_changed?</tt> attribute methods.
+ def attribute_previously_changed?(attr_name) # :nodoc:
+ mutations_before_last_save.changed?(attr_name.to_s)
end
# Restore all previous data of the provided attributes.
- def restore_attributes(attributes = changed)
- attributes.each { |attr| restore_attribute! attr }
+ def restore_attributes(attr_names = changed)
+ attr_names.each { |attr_name| restore_attribute!(attr_name) }
end
# Clears all dirty data: current changes and previous changes.
def clear_changes_information
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
@mutations_before_last_save = nil
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
forget_attribute_assignments
@mutations_from_database = nil
end
def clear_attribute_changes(attr_names)
- attributes_changed_by_setter.except!(*attr_names)
attr_names.each do |attr_name|
clear_attribute_change(attr_name)
end
@@ -216,13 +205,7 @@ module ActiveModel
# person.name = 'robert'
# person.changed_attributes # => {"name" => "bob"}
def changed_attributes
- # This should only be set by methods which will call changed_attributes
- # multiple times when it is known that the computed value cannot change.
- if defined?(@cached_changed_attributes)
- @cached_changed_attributes
- else
- attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
- end
+ mutations_from_database.changed_values
end
# Returns a hash of changed attributes indicating their original
@@ -232,9 +215,7 @@ module ActiveModel
# person.name = 'bob'
# person.changes # => { "name" => ["bill", "bob"] }
def changes
- cache_changed_attributes do
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
- end
+ mutations_from_database.changes
end
# Returns a hash of attributes that were changed before the model was saved.
@@ -244,27 +225,23 @@ module ActiveModel
# person.save
# person.previous_changes # => {"name" => ["bob", "robert"]}
def previous_changes
- @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
- @previously_changed.merge(mutations_before_last_save.changes)
+ mutations_before_last_save.changes
end
def attribute_changed_in_place?(attr_name) # :nodoc:
- mutations_from_database.changed_in_place?(attr_name)
+ mutations_from_database.changed_in_place?(attr_name.to_s)
end
private
def clear_attribute_change(attr_name)
- mutations_from_database.forget_change(attr_name)
+ mutations_from_database.forget_change(attr_name.to_s)
end
def mutations_from_database
- unless defined?(@mutations_from_database)
- @mutations_from_database = nil
- end
@mutations_from_database ||= if defined?(@attributes)
ActiveModel::AttributeMutationTracker.new(@attributes)
else
- NullMutationTracker.instance
+ ActiveModel::ForcedMutationTracker.new(self)
end
end
@@ -276,68 +253,28 @@ module ActiveModel
@mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
end
- def cache_changed_attributes
- @cached_changed_attributes = changed_attributes
- yield
- ensure
- clear_changed_attributes_cache
- end
-
- def clear_changed_attributes_cache
- remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
- end
-
- # Returns +true+ if attr_name is changed, +false+ otherwise.
- def changes_include?(attr_name)
- attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
- end
- alias attribute_changed_by_setter? changes_include?
-
- # Returns +true+ if attr_name were changed before the model was saved,
- # +false+ otherwise.
- def previous_changes_include?(attr_name)
- previous_changes.include?(attr_name)
- end
-
- # Handles <tt>*_change</tt> for +method_missing+.
- def attribute_change(attr)
- [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
+ # Dispatch target for <tt>*_change</tt> attribute methods.
+ def attribute_change(attr_name)
+ mutations_from_database.change_to_attribute(attr_name.to_s)
end
- # Handles <tt>*_previous_change</tt> for +method_missing+.
- def attribute_previous_change(attr)
- previous_changes[attr]
+ # Dispatch target for <tt>*_previous_change</tt> attribute methods.
+ def attribute_previous_change(attr_name)
+ mutations_before_last_save.change_to_attribute(attr_name.to_s)
end
- # Handles <tt>*_will_change!</tt> for +method_missing+.
- def attribute_will_change!(attr)
- unless attribute_changed?(attr)
- begin
- value = _read_attribute(attr)
- value = value.duplicable? ? value.clone : value
- rescue TypeError, NoMethodError
- end
-
- set_attribute_was(attr, value)
- end
- mutations_from_database.force_change(attr)
+ # Dispatch target for <tt>*_will_change!</tt> attribute methods.
+ def attribute_will_change!(attr_name)
+ mutations_from_database.force_change(attr_name.to_s)
end
- # Handles <tt>restore_*!</tt> for +method_missing+.
- def restore_attribute!(attr)
- if attribute_changed?(attr)
- __send__("#{attr}=", changed_attributes[attr])
- clear_attribute_changes([attr])
+ # Dispatch target for <tt>restore_*!</tt> attribute methods.
+ def restore_attribute!(attr_name)
+ attr_name = attr_name.to_s
+ if attribute_changed?(attr_name)
+ __send__("#{attr_name}=", attribute_was(attr_name))
+ clear_attribute_change(attr_name)
end
end
-
- def attributes_changed_by_setter
- @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
- end
-
- # Force an attribute to have a particular "before" value
- def set_attribute_was(attr, old_value)
- attributes_changed_by_setter[attr] = old_value
- end
end
end
diff --git a/activemodel/lib/active_model/error.rb b/activemodel/lib/active_model/error.rb
new file mode 100644
index 0000000000..6deab3578d
--- /dev/null
+++ b/activemodel/lib/active_model/error.rb
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/class/attribute"
+
+module ActiveModel
+ # == Active \Model \Error
+ #
+ # Represents one single error
+ class Error
+ CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
+ MESSAGE_OPTIONS = [:message]
+
+ class_attribute :i18n_customize_full_message, default: false
+
+ def self.full_message(attribute, message, base_class) # :nodoc:
+ return message if attribute == :base
+ attribute = attribute.to_s
+
+ if i18n_customize_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(defaults.shift,
+ default: defaults,
+ attribute: attr_name,
+ message: message)
+ end
+
+ def self.generate_message(attribute, type, base, options) # :nodoc:
+ type = options.delete(:message) if options[:message].is_a?(Symbol)
+ value = (attribute != :base ? base.send(:read_attribute_for_validation, attribute) : nil)
+
+ options = {
+ model: base.model_name.human,
+ attribute: base.class.human_attribute_name(attribute),
+ value: value,
+ object: base
+ }.merge!(options)
+
+ if base.class.respond_to?(:i18n_scope)
+ 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}"
+
+ catch(:exception) do
+ translation = I18n.translate(defaults.first, options.merge(default: defaults.drop(1), throw: true))
+ return translation unless translation.nil?
+ end unless options[:message]
+ else
+ defaults = []
+ end
+
+ defaults << :"errors.attributes.#{attribute}.#{type}"
+ defaults << :"errors.messages.#{type}"
+
+ key = defaults.shift
+ defaults = options.delete(:message) if options[:message]
+ options[:default] = defaults
+
+ I18n.translate(key, options)
+ end
+
+ 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
+ self.class.generate_message(attribute, raw_type, @base, 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
+ self.class.full_message(attribute, message, @base.class)
+ 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 == @options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)
+ 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.except(*CALLBACKS_OPTIONS)]
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 9de6b609a3..42c004ce31 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -4,6 +4,9 @@ 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_model/error"
+require "active_model/nested_error"
+require "forwardable"
module ActiveModel
# == Active \Model \Errors
@@ -59,15 +62,15 @@ 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!, :any?
+ # TODO: forward all enumerable methods after `each` deprecation is removed.
+ def_delegators :@errors, :count
- class << self
- attr_accessor :i18n_full_message # :nodoc:
- end
- self.i18n_full_message = false
+ LEGACY_ATTRIBUTES = [:messages, :details].freeze
- 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 +80,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 +98,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,18 +130,42 @@ 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
- # Clear the error messages.
+ # Removes all errors except the given keys. Returns a hash containing the removed errors.
#
- # person.errors.full_messages # => ["name cannot be nil"]
- # person.errors.clear
- # person.errors.full_messages # => []
- def clear
- messages.clear
- details.clear
+ # person.errors.keys # => [:name, :age, :gender, :city]
+ # 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)
+
+ results = messages.dup.slice!(*keys)
+
+ @errors.keep_if do |error|
+ keys.include?(error.attribute)
+ end
+
+ results
+ end
+
+ # Search for errors matching +attribute+, +type+ or +options+.
+ #
+ # Only supplied params will be matched.
+ #
+ # 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
@@ -129,8 +175,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?
@@ -140,10 +187,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).presence
end
# When passed a symbol or a name of a method, returns an array of errors
@@ -152,7 +202,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.
@@ -169,31 +219,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)
+ Enumerating ActiveModel::Errors as a hash has been deprecated.
+ In Rails 6.1, `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 two parameters,
+ so the old hash behavior is simulated. As this is deprecated,
+ this will result in an ArgumentError in Rails 6.2.
+ 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.
@@ -201,21 +257,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")
@@ -228,6 +275,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
@@ -247,13 +295,38 @@ 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 to_h
+ ActiveSupport::Deprecation.warn(<<~EOM)
+ ActiveModel::Errors#to_h is deprecated and will be removed in Rails 6.2
+ Please use `ActiveModel::Errors.to_hash` instead. The values in the hash
+ returned by `ActiveModel::Errors.to_hash` is an array of error messages.
+ EOM
+
+ to_hash.transform_values { |values| values.last }
+ 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+.
@@ -297,17 +370,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
@@ -317,22 +393,45 @@ module ActiveModel
# 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.
+ # If the error message requires options, then it returns +true+ with
+ # the correct options, or +false+ with incorrect or missing options.
+ #
+ # 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, type = :invalid, options = {})
+ attribute, type, options = normalize_arguments(attribute, type, options)
+
+ if type.is_a? Symbol
+ @errors.any? { |error|
+ error.strict_match?(attribute, type, options)
+ }
+ else
+ messages_for(attribute).include?(type)
+ end
+ end
+
+ # Returns +true+ if an error on the attribute with the given message is
+ # present, or +false+ otherwise. +message+ is treated the same as for +add+.
#
- # 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)
+ # person.errors.add :age
+ # person.errors.add :name, :too_long, { count: 25 }
+ # person.errors.of_kind? :age # => true
+ # person.errors.of_kind? :name # => false
+ # person.errors.of_kind? :name, :too_long # => true
+ # person.errors.of_kind? :name, "is too long (maximum is 25 characters)" # => true
+ # person.errors.of_kind? :name, :not_too_long # => false
+ # person.errors.of_kind? :name, "is too long" # => false
+ def of_kind?(attribute, message = :invalid)
+ attribute, message = normalize_arguments(attribute, message)
if message.is_a? Symbol
- details[attribute.to_sym].include? normalize_detail(message, options)
+ !where(attribute, message).empty?
else
- self[attribute].include? message
+ messages_for(attribute).include?(message)
end
end
@@ -347,7 +446,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
@@ -362,63 +461,18 @@ 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
-
- 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(defaults.shift,
- default: defaults,
- attribute: attr_name,
- message: message)
+ Error.full_message(attribute, message, @base.class)
end
# Translates an error message in its default scope
@@ -446,77 +500,112 @@ module ActiveModel
# * <tt>errors.attributes.title.blank</tt>
# * <tt>errors.messages.blank</tt>
def generate_message(attribute, type = :invalid, options = {})
- type = options.delete(:message) if options[:message].is_a?(Symbol)
+ Error.generate_message(attribute, type, @base, options)
+ end
+
+ def marshal_load(array) # :nodoc:
+ # Rails 5
+ @errors = []
+ @base = array[0]
+ add_from_legacy_details_hash(array[2])
+ end
- if @base.class.respond_to?(:i18n_scope)
- 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}" ]
+ def init_with(coder) # :nodoc:
+ 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_arguments(attribute, type, **options)
+ # Evaluate proc first
+ if type.respond_to?(:call)
+ type = type.call(@base, options)
end
- defaults << :"#{i18n_scope}.errors.messages.#{type}"
- else
- defaults = []
+
+ [attribute.to_sym, type, options]
end
- defaults << :"errors.attributes.#{attribute}.#{type}"
- defaults << :"errors.messages.#{type}"
+ def add_from_legacy_details_hash(details)
+ details.each { |attribute, errors|
+ errors.each { |error|
+ type = error.delete(:error)
+ add(attribute, type, error)
+ }
+ }
+ end
- key = defaults.shift
- defaults = options.delete(:message) if options[:message]
- value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
+ def deprecation_removal_warning(method_name)
+ ActiveSupport::Deprecation.warn("ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 6.2")
+ end
- options = {
- default: defaults,
- model: @base.model_name.human,
- attribute: @base.class.human_attribute_name(attribute),
- value: value,
- object: @base
- }.merge!(options)
+ 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
- I18n.translate(key, options)
+ class DeprecationHandlingMessageHash < SimpleDelegator
+ def initialize(errors)
+ @errors = errors
+ super(prepare_content)
end
- def marshal_dump # :nodoc:
- [@base, without_default_proc(@messages), without_default_proc(@details)]
- end
+ def []=(attribute, value)
+ ActiveSupport::Deprecation.warn("Calling `[]=` to an ActiveModel::Errors is deprecated. Please call `ActiveModel::Errors#add` instead.")
- def marshal_load(array) # :nodoc:
- @base, @messages, @details = array
- apply_default_array(@messages)
- apply_default_array(@details)
- end
+ @errors.delete(attribute)
+ Array(value).each do |message|
+ @errors.add(attribute, message)
+ 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)
+ __setobj__ prepare_content
end
- private
- def normalize_message(attribute, message, options)
- case message
- when Symbol
- generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
- else
- message
+ 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
+ end
- def normalize_detail(message, options)
- { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
+ class DeprecationHandlingMessageArray < SimpleDelegator
+ def initialize(content, errors, attribute)
+ @errors = errors
+ @attribute = attribute
+ super(content.freeze)
end
- def without_default_proc(hash)
- hash.dup.tap do |new_h|
- new_h.default_proc = nil
- end
+ 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
- def apply_default_array(hash)
- hash.default_proc = proc { |h, key| h[key] = [] }
- hash
+ class DeprecationHandlingDetailsHash < SimpleDelegator
+ def initialize(details)
+ details.default = []
+ details.freeze
+ super(details)
end
end
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
index cef5441e4a..5475c1eda7 100644
--- a/activemodel/lib/active_model/gem_version.rb
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -8,7 +8,7 @@ module ActiveModel
module VERSION
MAJOR = 6
- MINOR = 0
+ MINOR = 1
TINY = 0
PRE = "alpha"
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/naming.rb b/activemodel/lib/active_model/naming.rb
index bf23fa3c05..6a02d5dbf7 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -207,7 +207,6 @@ module ActiveModel
end
private
-
def _singularize(string)
ActiveSupport::Inflector.underscore(string).tr("/", "_")
end
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/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb
index 0ed70bd473..65e20b9791 100644
--- a/activemodel/lib/active_model/railtie.rb
+++ b/activemodel/lib/active_model/railtie.rb
@@ -13,8 +13,8 @@ module ActiveModel
ActiveModel::SecurePassword.min_cost = Rails.env.test?
end
- initializer "active_model.i18n_full_message" do
- ActiveModel::Errors.i18n_full_message = config.active_model.delete(:i18n_full_message) || false
+ initializer "active_model.i18n_customize_full_message" do
+ ActiveModel::Error.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
end
end
end
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index 51d54f34f3..5f409326bd 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -69,6 +69,27 @@ module ActiveModel
raise
end
+ include InstanceMethodsOnActivation.new(attribute)
+
+ if validations
+ include ActiveModel::Validations
+
+ # This ensures the model has a password by checking whether the password_digest
+ # is present, so that this works with both new and existing records. However,
+ # when there is an error, the message is added to the password attribute instead
+ # so that the error message will make sense to the end-user.
+ validate do |record|
+ record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
+ end
+
+ validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
+ validates_confirmation_of attribute, allow_blank: true
+ end
+ end
+ end
+
+ class InstanceMethodsOnActivation < Module
+ def initialize(attribute)
attr_reader attribute
define_method("#{attribute}=") do |unencrypted_password|
@@ -101,21 +122,6 @@ module ActiveModel
end
alias_method :authenticate, :authenticate_password if attribute == :password
-
- if validations
- include ActiveModel::Validations
-
- # This ensures the model has a password by checking whether the password_digest
- # is present, so that this works with both new and existing records. However,
- # when there is an error, the message is added to the password attribute instead
- # so that the error message will make sense to the end-user.
- validate do |record|
- record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
- end
-
- validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
- validates_confirmation_of attribute, allow_blank: true
- end
end
end
end
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index c4b7b32291..168dfa0dd2 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -150,7 +150,6 @@ module ActiveModel
end
private
-
# Hook method defining how an attribute value should be retrieved for
# serialization. By default this is assumed to be an instance named after
# the attribute. Override this method in subclasses should you need to
diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb
index f77fb98c32..09dcae889b 100644
--- a/activemodel/lib/active_model/serializers/json.rb
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -42,6 +42,13 @@ module ActiveModel
# # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
# # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
#
+ # If you prefer, <tt>:root</tt> may also be set to a custom string key instead as in:
+ #
+ # user = User.find(1)
+ # user.as_json(root: "author")
+ # # => { "author" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
+ #
# Without any +options+, the returned Hash will include all the model's
# attributes.
#
diff --git a/activemodel/lib/active_model/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb
index 89e43bcc5f..b2c3ee50aa 100644
--- a/activemodel/lib/active_model/type/big_integer.rb
+++ b/activemodel/lib/active_model/type/big_integer.rb
@@ -6,7 +6,6 @@ module ActiveModel
module Type
class BigInteger < Integer # :nodoc:
private
-
def max_value
::Float::INFINITY
end
diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb
index f6c6efbc87..1214e9319b 100644
--- a/activemodel/lib/active_model/type/boolean.rb
+++ b/activemodel/lib/active_model/type/boolean.rb
@@ -14,7 +14,16 @@ module ActiveModel
# - Empty strings are coerced to +nil+
# - All other values will be coerced to +true+
class Boolean < Value
- FALSE_VALUES = [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"].to_set
+ FALSE_VALUES = [
+ false, 0,
+ "0", :"0",
+ "f", :f,
+ "F", :F,
+ "false", :false,
+ "FALSE", :FALSE,
+ "off", :off,
+ "OFF", :OFF,
+ ].to_set.freeze
def type # :nodoc:
:boolean
@@ -25,7 +34,6 @@ module ActiveModel
end
private
-
def cast_value(value)
if value == ""
nil
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
index 8ec5deedc4..0e96d2c8a4 100644
--- a/activemodel/lib/active_model/type/date.rb
+++ b/activemodel/lib/active_model/type/date.rb
@@ -3,22 +3,18 @@
module ActiveModel
module Type
class Date < Value # :nodoc:
+ include Helpers::Timezone
include Helpers::AcceptsMultiparameterTime.new
def type
:date
end
- def serialize(value)
- cast(value)
- end
-
def type_cast_for_schema(value)
value.to_s(:db).inspect
end
private
-
def cast_value(value)
if value.is_a?(::String)
return if value.empty?
@@ -49,7 +45,7 @@ module ActiveModel
def value_from_multiparameter_assignment(*)
time = super
- time && time.to_date
+ time && new_date(time.year, time.mon, time.mday)
end
end
end
diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb
index d48598376e..ba705be9b2 100644
--- a/activemodel/lib/active_model/type/date_time.rb
+++ b/activemodel/lib/active_model/type/date_time.rb
@@ -3,6 +3,7 @@
module ActiveModel
module Type
class DateTime < Value # :nodoc:
+ include Helpers::Timezone
include Helpers::TimeValue
include Helpers::AcceptsMultiparameterTime.new(
defaults: { 4 => 0, 5 => 0 }
@@ -12,12 +13,7 @@ module ActiveModel
:datetime
end
- def serialize(value)
- super(cast(value))
- end
-
private
-
def cast_value(value)
return apply_seconds_precision(value) unless value.is_a?(::String)
return if value.empty?
diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb
index b37dad1c41..6aa51ff2ac 100644
--- a/activemodel/lib/active_model/type/decimal.rb
+++ b/activemodel/lib/active_model/type/decimal.rb
@@ -12,16 +12,11 @@ module ActiveModel
:decimal
end
- def serialize(value)
- cast(value)
- end
-
def type_cast_for_schema(value)
value.to_s.inspect
end
private
-
def cast_value(value)
casted_value = \
case value
diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb
index 9dbe32e5a6..36c7a5103c 100644
--- a/activemodel/lib/active_model/type/float.rb
+++ b/activemodel/lib/active_model/type/float.rb
@@ -18,10 +18,7 @@ module ActiveModel
end
end
- alias serialize cast
-
private
-
def cast_value(value)
case value
when ::Float then value
diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb
index 403f0a9e6b..20145d5f0d 100644
--- a/activemodel/lib/active_model/type/helpers.rb
+++ b/activemodel/lib/active_model/type/helpers.rb
@@ -4,3 +4,4 @@ require "active_model/type/helpers/accepts_multiparameter_time"
require "active_model/type/helpers/numeric"
require "active_model/type/helpers/mutable"
require "active_model/type/helpers/time_value"
+require "active_model/type/helpers/timezone"
diff --git a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
index ad891f841e..e15d7b013f 100644
--- a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
+++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
@@ -5,6 +5,10 @@ module ActiveModel
module Helpers # :nodoc: all
class AcceptsMultiparameterTime < Module
def initialize(defaults: {})
+ define_method(:serialize) do |value|
+ super(cast(value))
+ end
+
define_method(:cast) do |value|
if value.is_a?(Hash)
value_from_multiparameter_assignment(value)
diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb
index 473cdb0c67..074316b559 100644
--- a/activemodel/lib/active_model/type/helpers/numeric.rb
+++ b/activemodel/lib/active_model/type/helpers/numeric.rb
@@ -4,6 +4,10 @@ module ActiveModel
module Type
module Helpers # :nodoc: all
module Numeric
+ def serialize(value)
+ cast(value)
+ end
+
def cast(value)
value = \
case value
@@ -20,17 +24,19 @@ module ActiveModel
end
private
-
def number_to_non_number?(old_value, new_value_before_type_cast)
- old_value != nil && non_numeric_string?(new_value_before_type_cast)
+ old_value != nil && non_numeric_string?(new_value_before_type_cast.to_s)
end
def non_numeric_string?(value)
# 'wibble'.to_i will give zero, we want to make sure
# that we aren't marking int zero to string zero as
# changed.
- !/\A[-+]?\d+/.match?(value.to_s)
+ !NUMERIC_REGEX.match?(value)
end
+
+ NUMERIC_REGEX = /\A\s*[+-]?\d/
+ private_constant :NUMERIC_REGEX
end
end
end
diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb
index da56073436..075e906034 100644
--- a/activemodel/lib/active_model/type/helpers/time_value.rb
+++ b/activemodel/lib/active_model/type/helpers/time_value.rb
@@ -11,35 +11,30 @@ module ActiveModel
value = apply_seconds_precision(value)
if value.acts_like?(:time)
- zone_conversion_method = is_utc? ? :getutc : :getlocal
-
- if value.respond_to?(zone_conversion_method)
- value = value.send(zone_conversion_method)
+ if is_utc?
+ value = value.getutc if value.respond_to?(:getutc) && !value.utc?
+ else
+ value = value.getlocal if value.respond_to?(:getlocal)
end
end
value
end
- def is_utc?
- ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
- end
+ def apply_seconds_precision(value)
+ return value unless precision && value.respond_to?(:nsec)
+
+ number_of_insignificant_digits = 9 - precision
+ round_power = 10**number_of_insignificant_digits
+ rounded_off_nsec = value.nsec % round_power
- def default_timezone
- if is_utc?
- :utc
+ if rounded_off_nsec > 0
+ value.change(nsec: value.nsec - rounded_off_nsec)
else
- :local
+ value
end
end
- def apply_seconds_precision(value)
- return value unless precision && value.respond_to?(:usec)
- number_of_insignificant_digits = 6 - precision
- round_power = 10**number_of_insignificant_digits
- value.change(usec: value.usec - value.usec % round_power)
- end
-
def type_cast_for_schema(value)
value.to_s(:db).inspect
end
@@ -49,7 +44,6 @@ module ActiveModel
end
private
-
def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
# Treat 0000-00-00 00:00:00 as nil.
return if year.nil? || (year == 0 && mon == 0 && mday == 0)
diff --git a/activemodel/lib/active_model/type/helpers/timezone.rb b/activemodel/lib/active_model/type/helpers/timezone.rb
new file mode 100644
index 0000000000..cf87b9715b
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers/timezone.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/time/zones"
+
+module ActiveModel
+ module Type
+ module Helpers # :nodoc: all
+ module Timezone
+ def is_utc?
+ ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
+ end
+
+ def default_timezone
+ is_utc? ? :utc : :local
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/immutable_string.rb b/activemodel/lib/active_model/type/immutable_string.rb
index 826bd7038f..18e12c54d1 100644
--- a/activemodel/lib/active_model/type/immutable_string.rb
+++ b/activemodel/lib/active_model/type/immutable_string.rb
@@ -17,7 +17,6 @@ module ActiveModel
end
private
-
def cast_value(value)
result = \
case value
diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb
index da74aaa3c5..e9bbdf4b7b 100644
--- a/activemodel/lib/active_model/type/integer.rb
+++ b/activemodel/lib/active_model/type/integer.rb
@@ -19,34 +19,27 @@ module ActiveModel
end
def deserialize(value)
- return if value.nil?
+ return if value.blank?
value.to_i
end
def serialize(value)
- result = cast(value)
- if result
- ensure_in_range(result)
- end
- result
+ return if value.is_a?(::String) && non_numeric_string?(value)
+ ensure_in_range(super)
end
private
attr_reader :range
def cast_value(value)
- case value
- when true then 1
- when false then 0
- else
- value.to_i rescue nil
- end
+ value.to_i rescue nil
end
def ensure_in_range(value)
- unless range.cover?(value)
+ if value && !range.cover?(value)
raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
end
+ value
end
def max_value
diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb
index a9c9bfadb6..0d9f4a63b4 100644
--- a/activemodel/lib/active_model/type/string.rb
+++ b/activemodel/lib/active_model/type/string.rb
@@ -12,7 +12,6 @@ module ActiveModel
end
private
-
def cast_value(value)
case value
when ::String then ::String.new(value)
diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb
index b3056b1333..f230bd4257 100644
--- a/activemodel/lib/active_model/type/time.rb
+++ b/activemodel/lib/active_model/type/time.rb
@@ -3,9 +3,10 @@
module ActiveModel
module Type
class Time < Value # :nodoc:
+ include Helpers::Timezone
include Helpers::TimeValue
include Helpers::AcceptsMultiparameterTime.new(
- defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
+ defaults: { 1 => 2000, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
)
def type
@@ -28,7 +29,6 @@ module ActiveModel
end
private
-
def cast_value(value)
return apply_seconds_precision(value) unless value.is_a?(::String)
return if value.empty?
diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb
index b6914dd63c..788ded3e96 100644
--- a/activemodel/lib/active_model/type/value.rb
+++ b/activemodel/lib/active_model/type/value.rb
@@ -110,11 +110,10 @@ module ActiveModel
[self.class, precision, scale, limit].hash
end
- def assert_valid_value(*)
+ def assert_valid_value(_)
end
private
-
# Convenience method for types which do not need separate type casting
# behavior for user and database inputs. Called by Value#cast for
# values except +nil+.
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index 7f14d102dd..4a6b464131 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
require "active_support/core_ext/array/extract_options"
-require "active_support/core_ext/hash/keys"
-require "active_support/core_ext/hash/except"
module ActiveModel
# == Active \Model \Validations
@@ -404,7 +402,6 @@ module ActiveModel
alias :read_attribute_for_validation :send
private
-
def run_validations!
_run_validate_callbacks
errors.empty?
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index ea3a6b52ab..1b96575a10 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -15,7 +15,6 @@ module ActiveModel
end
private
-
def setup!(klass)
klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes)))
end
@@ -54,8 +53,9 @@ module ActiveModel
def define_on(klass)
attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
- klass.send(:attr_reader, *attr_readers)
- klass.send(:attr_writer, *attr_writers)
+ klass.define_attribute_methods
+ klass.attr_reader(*attr_readers)
+ klass.attr_writer(*attr_writers)
end
private
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
index 887d31ae2a..7178ba99e9 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -112,7 +112,6 @@ module ActiveModel
end
private
-
# Overwrite run validations to include callbacks.
def run_validations!
_run_validation_callbacks { super }
diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb
index bafb8e2106..fb9f48301c 100644
--- a/activemodel/lib/active_model/validations/clusivity.rb
+++ b/activemodel/lib/active_model/validations/clusivity.rb
@@ -15,7 +15,6 @@ module ActiveModel
end
private
-
def include?(record, value)
members = if delimiter.respond_to?(:call)
delimiter.call(record)
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index 1b5d5b09ab..b549755ba4 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -19,11 +19,11 @@ module ActiveModel
private
def setup!(klass)
- klass.send(:attr_reader, *attributes.map do |attribute|
+ klass.attr_reader(*attributes.map do |attribute|
:"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
end.compact)
- klass.send(:attr_writer, *attributes.map do |attribute|
+ klass.attr_writer(*attributes.map do |attribute|
:"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
end.compact)
end
diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb
index 7c3f091473..bea57415b0 100644
--- a/activemodel/lib/active_model/validations/format.rb
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -23,7 +23,6 @@ module ActiveModel
end
private
-
def option_call(record, name)
option = options[name]
option.respond_to?(:call) ? option.call(record) : option
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index d6c80b2c5d..02759b4ccb 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -32,7 +32,7 @@ module ActiveModel
value = options[key]
unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc)
- raise ArgumentError, ":#{key} must be a nonnegative Integer, Infinity, Symbol, or Proc"
+ raise ArgumentError, ":#{key} must be a non-negative Integer, Infinity, Symbol, or Proc"
end
end
end
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index 126a87ac6e..e7be668e9d 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "bigdecimal/util"
+
module ActiveModel
module Validations
class NumericalityValidator < EachValidator # :nodoc:
@@ -9,6 +11,8 @@ module ActiveModel
RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
+ INTEGER_REGEX = /\A[+-]?\d+\z/
+
def check_validity!
keys = CHECKS.keys - [:odd, :even]
options.slice(*keys).each do |option, value|
@@ -49,11 +53,7 @@ module ActiveModel
return
end
- if raw_value.is_a?(Numeric)
- value = raw_value
- else
- value = parse_raw_value_as_a_number(raw_value)
- end
+ value = parse_as_number(raw_value)
options.slice(*CHECKS.keys).each do |option, option_value|
case option
@@ -69,6 +69,8 @@ module ActiveModel
option_value = record.send(option_value)
end
+ option_value = parse_as_number(option_value)
+
unless value.send(CHECKS[option], option_value)
record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value))
end
@@ -77,20 +79,30 @@ module ActiveModel
end
private
-
def is_number?(raw_value)
- !parse_raw_value_as_a_number(raw_value).nil?
+ !parse_as_number(raw_value).nil?
rescue ArgumentError, TypeError
false
end
- def parse_raw_value_as_a_number(raw_value)
- return raw_value.to_i if is_integer?(raw_value)
- Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
+ def parse_as_number(raw_value)
+ if raw_value.is_a?(Float)
+ raw_value.to_d
+ elsif raw_value.is_a?(Numeric)
+ raw_value
+ elsif is_integer?(raw_value)
+ raw_value.to_i
+ elsif !is_hexadecimal_literal?(raw_value)
+ Kernel.Float(raw_value).to_d
+ end
end
def is_integer?(raw_value)
- /\A[+-]?\d+\z/ === raw_value.to_s
+ INTEGER_REGEX.match?(raw_value.to_s)
+ end
+
+ def is_hexadecimal_literal?(raw_value)
+ /\A0[xX]/.match?(raw_value.to_s)
end
def filtered_options(value)
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
index 21c4ce0dfe..97612d474d 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -150,7 +150,6 @@ module ActiveModel
end
private
-
# When creating custom validators, it might be useful to be able to specify
# additional default keys. This can be done by overwriting this method.
def _validates_default_keys
diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb
index e17c3ca7b3..3ba6acea15 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -90,7 +90,7 @@ module ActiveModel
# class MyValidator < ActiveModel::Validator
# def initialize(options={})
# super
- # options[:class].send :attr_accessor, :custom_attribute
+ # options[:class].attr_accessor :custom_attribute
# end
# end
class Validator
@@ -175,7 +175,6 @@ module ActiveModel
end
private
-
def validate_each(record, attribute, value)
@block.call(record, attribute, value)
end