diff options
Diffstat (limited to 'activemodel/lib/active_model')
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 } |