aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib/active_model
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel/lib/active_model')
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb29
-rw-r--r--activemodel/lib/active_model/attribute_mutation_tracker.rb117
-rw-r--r--activemodel/lib/active_model/attributes.rb61
-rw-r--r--activemodel/lib/active_model/dirty.rb133
-rw-r--r--activemodel/lib/active_model/error.rb81
-rw-r--r--activemodel/lib/active_model/errors.rb386
-rw-r--r--activemodel/lib/active_model/gem_version.rb4
-rw-r--r--activemodel/lib/active_model/lint.rb2
-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/type/boolean.rb11
-rw-r--r--activemodel/lib/active_model/type/date.rb7
-rw-r--r--activemodel/lib/active_model/type/date_time.rb5
-rw-r--r--activemodel/lib/active_model/type/decimal.rb4
-rw-r--r--activemodel/lib/active_model/type/float.rb2
-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.rb11
-rw-r--r--activemodel/lib/active_model/type/helpers/time_value.rb12
-rw-r--r--activemodel/lib/active_model/type/helpers/timezone.rb19
-rw-r--r--activemodel/lib/active_model/type/integer.rb19
-rw-r--r--activemodel/lib/active_model/type/time.rb1
23 files changed, 638 insertions, 344 deletions
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 5c4670f393..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
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/attributes.rb b/activemodel/lib/active_model/attributes.rb
index c3a446098c..d176ea88d0 100644
--- a/activemodel/lib/active_model/attributes.rb
+++ b/activemodel/lib/active_model/attributes.rb
@@ -26,6 +26,21 @@ module ActiveModel
define_attribute_method(name)
end
+ # 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)
@@ -65,33 +80,57 @@ 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
+ # 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/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 0d9e761b1e..35a587658c 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"
@@ -145,10 +140,9 @@ module ActiveModel
# +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..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 9fd6f2d89c..d7bcfacce3 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!, :any?
+ # TODO: forward all enumerable methods after `each` deprecation is removed.
+ def_delegators :@errors, :count
+
+ LEGACY_ATTRIBUTES = [:messages, :details].freeze
class << self
- attr_accessor :i18n_full_message # :nodoc:
+ attr_accessor :i18n_customize_full_message # :nodoc:
end
- self.i18n_full_message = false
+ 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)
+ 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.
@@ -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,26 +457,21 @@ 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)
+ if self.class.i18n_customize_full_message && @base.class.respond_to?(:i18n_scope)
attribute = attribute.remove(/\[\d\]/)
parts = attribute.split(".")
attribute_name = parts.pop
@@ -479,6 +537,14 @@ module ActiveModel
# * <tt>errors.messages.blank</tt>
def generate_message(attribute, type = :invalid, options = {})
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
@@ -487,6 +553,11 @@ module ActiveModel
:"#{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
@@ -496,59 +567,116 @@ module ActiveModel
key = defaults.shift
defaults = options.delete(:message) if options[:message]
- value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
-
- options = {
- default: defaults,
- model: @base.model_name.human,
- attribute: @base.class.human_attribute_name(attribute),
- value: value,
- object: @base
- }.merge!(options)
+ options[:default] = defaults
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.2")
+ 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/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
index b0322f62c7..5475c1eda7 100644
--- a/activemodel/lib/active_model/gem_version.rb
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -8,9 +8,9 @@ module ActiveModel
module VERSION
MAJOR = 6
- MINOR = 0
+ MINOR = 1
TINY = 0
- PRE = "beta1"
+ PRE = "alpha"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
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/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb
index 0ed70bd473..eb7901c7e9 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::Errors.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/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb
index f6c6efbc87..e64d2c793c 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
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
index 8ec5deedc4..c5fe926039 100644
--- a/activemodel/lib/active_model/type/date.rb
+++ b/activemodel/lib/active_model/type/date.rb
@@ -3,16 +3,13 @@
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
@@ -49,7 +46,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..133410e821 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,10 +13,6 @@ module ActiveModel
:datetime
end
- def serialize(value)
- super(cast(value))
- end
-
private
def cast_value(value)
diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb
index b37dad1c41..e8ee18c00e 100644
--- a/activemodel/lib/active_model/type/decimal.rb
+++ b/activemodel/lib/active_model/type/decimal.rb
@@ -12,10 +12,6 @@ module ActiveModel
:decimal
end
- def serialize(value)
- cast(value)
- end
-
def type_cast_for_schema(value)
value.to_s.inspect
end
diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb
index 9dbe32e5a6..ea1987df7c 100644
--- a/activemodel/lib/active_model/type/float.rb
+++ b/activemodel/lib/active_model/type/float.rb
@@ -18,8 +18,6 @@ module ActiveModel
end
end
- alias serialize cast
-
private
def cast_value(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..1d8171e25b 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
@@ -22,15 +26,18 @@ module ActiveModel
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..735b9a75a6 100644
--- a/activemodel/lib/active_model/type/helpers/time_value.rb
+++ b/activemodel/lib/active_model/type/helpers/time_value.rb
@@ -21,18 +21,6 @@ module ActiveModel
value
end
- def is_utc?
- ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
- end
-
- def default_timezone
- if is_utc?
- :utc
- else
- :local
- end
- end
-
def apply_seconds_precision(value)
return value unless precision && value.respond_to?(:usec)
number_of_insignificant_digits = 6 - precision
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/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/time.rb b/activemodel/lib/active_model/type/time.rb
index 8fba01b86a..61847a4ce7 100644
--- a/activemodel/lib/active_model/type/time.rb
+++ b/activemodel/lib/active_model/type/time.rb
@@ -3,6 +3,7 @@
module ActiveModel
module Type
class Time < Value # :nodoc:
+ include Helpers::Timezone
include Helpers::TimeValue
include Helpers::AcceptsMultiparameterTime.new(
defaults: { 1 => 2000, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }