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.rb247
-rw-r--r--activemodel/lib/active_model/attribute/user_provided_default.rb30
-rw-r--r--activemodel/lib/active_model/attribute_mutation_tracker.rb117
-rw-r--r--activemodel/lib/active_model/attribute_set.rb114
-rw-r--r--activemodel/lib/active_model/attribute_set/builder.rb124
-rw-r--r--activemodel/lib/active_model/attribute_set/yaml_encoder.rb41
-rw-r--r--activemodel/lib/active_model/attributes.rb107
-rw-r--r--activemodel/lib/active_model/dirty.rb181
-rw-r--r--activemodel/lib/active_model/errors.rb10
-rw-r--r--activemodel/lib/active_model/gem_version.rb2
-rw-r--r--activemodel/lib/active_model/naming.rb4
-rw-r--r--activemodel/lib/active_model/type.rb38
-rw-r--r--activemodel/lib/active_model/type/big_integer.rb2
-rw-r--r--activemodel/lib/active_model/type/helpers.rb8
-rw-r--r--activemodel/lib/active_model/type/string.rb2
-rw-r--r--activemodel/lib/active_model/validations.rb6
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb24
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb2
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb2
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb2
-rw-r--r--activemodel/lib/active_model/validations/length.rb10
-rw-r--r--activemodel/lib/active_model/validations/validates.rb2
22 files changed, 956 insertions, 119 deletions
diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb
new file mode 100644
index 0000000000..27f3e38e31
--- /dev/null
+++ b/activemodel/lib/active_model/attribute.rb
@@ -0,0 +1,247 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/duplicable"
+
+module ActiveModel
+ class Attribute # :nodoc:
+ class << self
+ def from_database(name, value, type)
+ FromDatabase.new(name, value, type)
+ end
+
+ def from_user(name, value, type, original_attribute = nil)
+ FromUser.new(name, value, type, original_attribute)
+ end
+
+ def with_cast_value(name, value, type)
+ WithCastValue.new(name, value, type)
+ end
+
+ def null(name)
+ Null.new(name)
+ end
+
+ def uninitialized(name, type)
+ Uninitialized.new(name, type)
+ end
+ end
+
+ attr_reader :name, :value_before_type_cast, :type
+
+ # This method should not be called directly.
+ # Use #from_database or #from_user
+ def initialize(name, value_before_type_cast, type, original_attribute = nil)
+ @name = name
+ @value_before_type_cast = value_before_type_cast
+ @type = type
+ @original_attribute = original_attribute
+ end
+
+ def value
+ # `defined?` is cheaper than `||=` when we get back falsy values
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
+ @value
+ end
+
+ def original_value
+ if assigned?
+ original_attribute.original_value
+ else
+ type_cast(value_before_type_cast)
+ end
+ end
+
+ def value_for_database
+ type.serialize(value)
+ end
+
+ def changed?
+ changed_from_assignment? || changed_in_place?
+ end
+
+ def changed_in_place?
+ has_been_read? && type.changed_in_place?(original_value_for_database, value)
+ end
+
+ def forgetting_assignment
+ with_value_from_database(value_for_database)
+ end
+
+ def with_value_from_user(value)
+ type.assert_valid_value(value)
+ self.class.from_user(name, value, type, original_attribute || self)
+ end
+
+ def with_value_from_database(value)
+ self.class.from_database(name, value, type)
+ end
+
+ def with_cast_value(value)
+ self.class.with_cast_value(name, value, type)
+ end
+
+ def with_type(type)
+ if changed_in_place?
+ with_value_from_user(value).with_type(type)
+ else
+ self.class.new(name, value_before_type_cast, type, original_attribute)
+ end
+ end
+
+ def type_cast(*)
+ raise NotImplementedError
+ end
+
+ def initialized?
+ true
+ end
+
+ def came_from_user?
+ false
+ end
+
+ def has_been_read?
+ defined?(@value)
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ name == other.name &&
+ value_before_type_cast == other.value_before_type_cast &&
+ type == other.type
+ end
+ alias eql? ==
+
+ def hash
+ [self.class, name, value_before_type_cast, type].hash
+ end
+
+ def init_with(coder)
+ @name = coder["name"]
+ @value_before_type_cast = coder["value_before_type_cast"]
+ @type = coder["type"]
+ @original_attribute = coder["original_attribute"]
+ @value = coder["value"] if coder.map.key?("value")
+ end
+
+ def encode_with(coder)
+ coder["name"] = name
+ coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil?
+ coder["type"] = type if type
+ coder["original_attribute"] = original_attribute if original_attribute
+ coder["value"] = value if defined?(@value)
+ end
+
+ protected
+
+ attr_reader :original_attribute
+ alias_method :assigned?, :original_attribute
+
+ def original_value_for_database
+ if assigned?
+ original_attribute.original_value_for_database
+ else
+ _original_value_for_database
+ end
+ end
+
+ private
+ def initialize_dup(other)
+ if defined?(@value) && @value.duplicable?
+ @value = @value.dup
+ end
+ end
+
+ def changed_from_assignment?
+ assigned? && type.changed?(original_value, value, value_before_type_cast)
+ end
+
+ def _original_value_for_database
+ type.serialize(original_value)
+ end
+
+ class FromDatabase < Attribute # :nodoc:
+ def type_cast(value)
+ type.deserialize(value)
+ end
+
+ def _original_value_for_database
+ value_before_type_cast
+ end
+ end
+
+ class FromUser < Attribute # :nodoc:
+ def type_cast(value)
+ type.cast(value)
+ end
+
+ def came_from_user?
+ !type.value_constructed_by_mass_assignment?(value_before_type_cast)
+ end
+ end
+
+ class WithCastValue < Attribute # :nodoc:
+ def type_cast(value)
+ value
+ end
+
+ def changed_in_place?
+ false
+ end
+ end
+
+ class Null < Attribute # :nodoc:
+ def initialize(name)
+ super(name, nil, Type.default_value)
+ end
+
+ def type_cast(*)
+ nil
+ end
+
+ def with_type(type)
+ self.class.with_cast_value(name, nil, type)
+ end
+
+ def with_value_from_database(value)
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
+ end
+ alias_method :with_value_from_user, :with_value_from_database
+ end
+
+ class Uninitialized < Attribute # :nodoc:
+ UNINITIALIZED_ORIGINAL_VALUE = Object.new
+
+ def initialize(name, type)
+ super(name, nil, type)
+ end
+
+ def value
+ if block_given?
+ yield name
+ end
+ end
+
+ def original_value
+ UNINITIALIZED_ORIGINAL_VALUE
+ end
+
+ def value_for_database
+ end
+
+ def initialized?
+ false
+ end
+
+ def forgetting_assignment
+ dup
+ end
+
+ def with_type(type)
+ self.class.new(name, type)
+ end
+ end
+
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
+ end
+end
diff --git a/activemodel/lib/active_model/attribute/user_provided_default.rb b/activemodel/lib/active_model/attribute/user_provided_default.rb
new file mode 100644
index 0000000000..f274b687d4
--- /dev/null
+++ b/activemodel/lib/active_model/attribute/user_provided_default.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "active_model/attribute"
+
+module ActiveModel
+ class Attribute # :nodoc:
+ class UserProvidedDefault < FromUser # :nodoc:
+ def initialize(name, value, type, database_default)
+ @user_provided_value = value
+ super(name, value, type, database_default)
+ end
+
+ def value_before_type_cast
+ if user_provided_value.is_a?(Proc)
+ @memoized_value_before_type_cast ||= user_provided_value.call
+ else
+ @user_provided_value
+ end
+ end
+
+ def with_type(type)
+ self.class.new(name, user_provided_value, type, original_attribute)
+ end
+
+ protected
+
+ attr_reader :user_provided_value
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb
new file mode 100644
index 0000000000..f55613ecd5
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+
+module ActiveModel
+ class AttributeMutationTracker # :nodoc:
+ OPTION_NOT_GIVEN = Object.new
+
+ def initialize(attributes)
+ @attributes = attributes
+ @forced_changes = Set.new
+ end
+
+ 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
+ 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
+ result[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)]
+ end
+ end
+
+ def changed_attribute_names
+ attr_names.select { |attr| changed?(attr) }
+ end
+
+ def any_changes?
+ attr_names.any? { |attr| changed?(attr) }
+ 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)
+ end
+
+ def changed_in_place?(attr_name)
+ attributes[attr_name.to_s].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
+ end
+
+ def force_change(attr_name)
+ forced_changes << attr_name.to_s
+ end
+
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
+ # Workaround for Ruby 2.2 "private attribute?" warning.
+ protected
+
+ attr_reader :attributes, :forced_changes
+
+ private
+
+ def attr_names
+ attributes.keys
+ end
+ end
+
+ class NullMutationTracker # :nodoc:
+ include Singleton
+
+ def changed_values(*)
+ {}
+ end
+
+ def changes(*)
+ {}
+ end
+
+ def change_to_attribute(attr_name)
+ end
+
+ def any_changes?(*)
+ false
+ end
+
+ def changed?(*)
+ false
+ end
+
+ def changed_in_place?(*)
+ false
+ end
+
+ def forget_change(*)
+ end
+
+ def original_value(*)
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attribute_set.rb b/activemodel/lib/active_model/attribute_set.rb
new file mode 100644
index 0000000000..a890ee3932
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_set.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/deep_dup"
+require "active_model/attribute_set/builder"
+require "active_model/attribute_set/yaml_encoder"
+
+module ActiveModel
+ class AttributeSet # :nodoc:
+ delegate :each_value, :fetch, :except, to: :attributes
+
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def [](name)
+ attributes[name] || Attribute.null(name)
+ end
+
+ def []=(name, value)
+ attributes[name] = value
+ end
+
+ def values_before_type_cast
+ attributes.transform_values(&:value_before_type_cast)
+ end
+
+ def to_hash
+ initialized_attributes.transform_values(&:value)
+ end
+ alias_method :to_h, :to_hash
+
+ def key?(name)
+ attributes.key?(name) && self[name].initialized?
+ end
+
+ def keys
+ attributes.each_key.select { |name| self[name].initialized? }
+ end
+
+ if defined?(JRUBY_VERSION)
+ # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
+ # https://github.com/jruby/jruby/pull/2562
+ def fetch_value(name, &block)
+ self[name].value(&block)
+ end
+ else
+ def fetch_value(name)
+ self[name].value { |n| yield n if block_given? }
+ end
+ end
+
+ def write_from_database(name, value)
+ attributes[name] = self[name].with_value_from_database(value)
+ end
+
+ def write_from_user(name, value)
+ attributes[name] = self[name].with_value_from_user(value)
+ end
+
+ def write_cast_value(name, value)
+ attributes[name] = self[name].with_cast_value(value)
+ end
+
+ def freeze
+ @attributes.freeze
+ super
+ end
+
+ def deep_dup
+ self.class.allocate.tap do |copy|
+ copy.instance_variable_set(:@attributes, attributes.deep_dup)
+ end
+ end
+
+ def initialize_dup(_)
+ @attributes = attributes.dup
+ super
+ end
+
+ def initialize_clone(_)
+ @attributes = attributes.clone
+ super
+ end
+
+ def reset(key)
+ if key?(key)
+ write_from_database(key, nil)
+ end
+ end
+
+ def accessed
+ attributes.select { |_, attr| attr.has_been_read? }.keys
+ end
+
+ def map(&block)
+ new_attributes = attributes.transform_values(&block)
+ AttributeSet.new(new_attributes)
+ end
+
+ def ==(other)
+ attributes == other.attributes
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def initialized_attributes
+ attributes.select { |_, attr| attr.initialized? }
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attribute_set/builder.rb b/activemodel/lib/active_model/attribute_set/builder.rb
new file mode 100644
index 0000000000..758eb830fc
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_set/builder.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require "active_model/attribute"
+
+module ActiveModel
+ class AttributeSet # :nodoc:
+ class Builder # :nodoc:
+ attr_reader :types, :default_attributes
+
+ def initialize(types, default_attributes = {})
+ @types = types
+ @default_attributes = default_attributes
+ end
+
+ def build_from_database(values = {}, additional_types = {})
+ attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
+ AttributeSet.new(attributes)
+ end
+ end
+ end
+
+ class LazyAttributeHash # :nodoc:
+ delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize
+
+ def initialize(types, values, additional_types, default_attributes)
+ @types = types
+ @values = values
+ @additional_types = additional_types
+ @materialized = false
+ @delegate_hash = {}
+ @default_attributes = default_attributes
+ end
+
+ def key?(key)
+ delegate_hash.key?(key) || values.key?(key) || types.key?(key)
+ end
+
+ def [](key)
+ delegate_hash[key] || assign_default_value(key)
+ end
+
+ def []=(key, value)
+ if frozen?
+ raise RuntimeError, "Can't modify frozen hash"
+ end
+ delegate_hash[key] = value
+ end
+
+ def deep_dup
+ dup.tap do |copy|
+ copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
+ end
+ end
+
+ def initialize_dup(_)
+ @delegate_hash = Hash[delegate_hash]
+ super
+ end
+
+ def select
+ keys = types.keys | values.keys | delegate_hash.keys
+ keys.each_with_object({}) do |key, hash|
+ attribute = self[key]
+ if yield(key, attribute)
+ hash[key] = attribute
+ end
+ end
+ end
+
+ def ==(other)
+ if other.is_a?(LazyAttributeHash)
+ materialize == other.materialize
+ else
+ materialize == other
+ end
+ end
+
+ def marshal_dump
+ materialize
+ end
+
+ def marshal_load(delegate_hash)
+ @delegate_hash = delegate_hash
+ @types = {}
+ @values = {}
+ @additional_types = {}
+ @materialized = true
+ end
+
+ protected
+
+ attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
+
+ def materialize
+ unless @materialized
+ values.each_key { |key| self[key] }
+ types.each_key { |key| self[key] }
+ unless frozen?
+ @materialized = true
+ end
+ end
+ delegate_hash
+ end
+
+ private
+
+ def assign_default_value(name)
+ type = additional_types.fetch(name, types[name])
+ value_present = true
+ value = values.fetch(name) { value_present = false }
+
+ if value_present
+ delegate_hash[name] = Attribute.from_database(name, value, type)
+ elsif types.key?(name)
+ attr = default_attributes[name]
+ if attr
+ delegate_hash[name] = attr.dup
+ else
+ delegate_hash[name] = Attribute.uninitialized(name, type)
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attribute_set/yaml_encoder.rb b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb
new file mode 100644
index 0000000000..4ea945b956
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ class AttributeSet
+ # Attempts to do more intelligent YAML dumping of an
+ # ActiveModel::AttributeSet to reduce the size of the resulting string
+ class YAMLEncoder # :nodoc:
+ def initialize(default_types)
+ @default_types = default_types
+ end
+
+ def encode(attribute_set, coder)
+ coder["concise_attributes"] = attribute_set.each_value.map do |attr|
+ if attr.type.equal?(default_types[attr.name])
+ attr.with_type(nil)
+ else
+ attr
+ end
+ end
+ end
+
+ def decode(coder)
+ if coder["attributes"]
+ coder["attributes"]
+ else
+ attributes_hash = Hash[coder["concise_attributes"].map do |attr|
+ if attr.type.nil?
+ attr = attr.with_type(default_types[attr.name])
+ end
+ [attr.name, attr]
+ end]
+ AttributeSet.new(attributes_hash)
+ end
+ end
+
+ protected
+
+ attr_reader :default_types
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb
new file mode 100644
index 0000000000..046ae67ad7
--- /dev/null
+++ b/activemodel/lib/active_model/attributes.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require "active_model/attribute_set"
+require "active_model/attribute/user_provided_default"
+
+module ActiveModel
+ module Attributes #:nodoc:
+ extend ActiveSupport::Concern
+ include ActiveModel::AttributeMethods
+
+ included do
+ attribute_method_suffix "="
+ class_attribute :attribute_types, :_default_attributes, instance_accessor: false
+ self.attribute_types = Hash.new(Type.default_value)
+ self._default_attributes = AttributeSet.new({})
+ end
+
+ module ClassMethods
+ def attribute(name, type = Type::Value.new, **options)
+ name = name.to_s
+ if type.is_a?(Symbol)
+ type = ActiveModel::Type.lookup(type, **options.except(:default))
+ end
+ self.attribute_types = attribute_types.merge(name => type)
+ define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type)
+ define_attribute_method(name)
+ end
+
+ private
+
+ def define_method_attribute=(name)
+ safe_name = name.unpack("h*".freeze).first
+ ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__#{safe_name}=(value)
+ name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ write_attribute(name, value)
+ end
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
+ undef_method :__temp__#{safe_name}=
+ STR
+ end
+
+ NO_DEFAULT_PROVIDED = Object.new # :nodoc:
+ private_constant :NO_DEFAULT_PROVIDED
+
+ def define_default_attribute(name, value, type)
+ self._default_attributes = _default_attributes.deep_dup
+ if value == NO_DEFAULT_PROVIDED
+ default_attribute = _default_attributes[name].with_type(type)
+ else
+ default_attribute = Attribute::UserProvidedDefault.new(
+ name,
+ value,
+ type,
+ _default_attributes.fetch(name.to_s) { nil },
+ )
+ end
+ _default_attributes[name] = default_attribute
+ end
+ end
+
+ def initialize(*)
+ @attributes = self.class._default_attributes.deep_dup
+ super
+ 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
+
+ @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
+ @attributes.fetch_value(name)
+ end
+
+ # Handle *= for method_missing.
+ def attribute=(attribute_name, value)
+ write_attribute(attribute_name, value)
+ end
+ end
+
+ module AttributeMethods #:nodoc:
+ AttrNames = Module.new {
+ def self.set_name_cache(name, value)
+ const_name = "ATTR_#{name}"
+ unless const_defined? const_name
+ const_set const_name, value.dup.freeze
+ end
+ end
+ }
+ end
+end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 943db0ab52..0044fde6c5 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -2,6 +2,8 @@
require "active_support/hash_with_indifferent_access"
require "active_support/core_ext/object/duplicable"
+require "active_model/attribute_mutation_tracker"
+require "active_model/attribute_set"
module ActiveModel
# == Active \Model \Dirty
@@ -130,13 +132,30 @@ module ActiveModel
attribute_method_affix prefix: "restore_", suffix: "!"
end
+ def initialize_dup(other) # :nodoc:
+ super
+ if self.class.respond_to?(:_default_attributes)
+ @attributes = self.class._default_attributes.map do |attr|
+ attr.with_value_from_user(@attributes.fetch_value(attr.name))
+ end
+ end
+ @mutations_from_database = nil
+ end
+
+ def changes_applied # :nodoc:
+ _prepare_changes
+ @mutations_before_last_save = mutations_from_database
+ forget_attribute_assignments
+ @mutations_from_database = nil
+ end
+
# Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
#
# person.changed? # => false
# 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.
@@ -145,7 +164,52 @@ 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:
+ !!mutations_from_database.changed?(attr) &&
+ (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
+ (from == OPTION_NOT_GIVEN || from == attribute_was(attr))
+ end
+
+ # Handles <tt>*_was</tt> for +method_missing+.
+ def attribute_was(attr) # :nodoc:
+ mutations_from_database.original_value(attr)
+ end
+
+ # Handles <tt>*_previously_changed?</tt> for +method_missing+.
+ def attribute_previously_changed?(attr) #:nodoc:
+ mutations_before_last_save.changed?(attr)
+ end
+
+ # Restore all previous data of the provided attributes.
+ def restore_attributes(attributes = changed)
+ attributes.each { |attr| restore_attribute! attr }
+ end
+
+ # Clears all dirty data: current changes and previous changes.
+ def clear_changes_information
+ @mutations_before_last_save = nil
+ forget_attribute_assignments
+ @mutations_from_database = nil
+ end
+
+ def clear_attribute_changes(attr_names)
+ attr_names.each do |attr_name|
+ clear_attribute_change(attr_name)
+ end
+ end
+
+ # Returns a hash of the attributes with unsaved changes indicating their original
+ # values like <tt>attr => original value</tt>.
+ #
+ # person.name # => "bob"
+ # person.name = 'robert'
+ # person.changed_attributes # => {"name" => "bob"}
+ def changed_attributes
+ mutations_from_database.changed_values.freeze
end
# Returns a hash of changed attributes indicating their original
@@ -155,7 +219,8 @@ module ActiveModel
# person.name = 'bob'
# person.changes # => { "name" => ["bill", "bob"] }
def changes
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
+ _prepare_changes
+ mutations_from_database.changes
end
# Returns a hash of attributes that were changed before the model was saved.
@@ -165,110 +230,82 @@ module ActiveModel
# person.save
# person.previous_changes # => {"name" => ["bob", "robert"]}
def previous_changes
- @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
- end
-
- # Returns a hash of the attributes with unsaved changes indicating their original
- # values like <tt>attr => original value</tt>.
- #
- # person.name # => "bob"
- # person.name = 'robert'
- # person.changed_attributes # => {"name" => "bob"}
- def changed_attributes
- @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
- 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])
- end
-
- # Handles <tt>*_was</tt> for +method_missing+.
- def attribute_was(attr) # :nodoc:
- attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
+ mutations_before_last_save.changes
end
- # Handles <tt>*_previously_changed?</tt> for +method_missing+.
- def attribute_previously_changed?(attr) #:nodoc:
- previous_changes_include?(attr)
- end
-
- # Restore all previous data of the provided attributes.
- def restore_attributes(attributes = changed)
- attributes.each { |attr| restore_attribute! attr }
+ def attribute_changed_in_place?(attr_name) # :nodoc:
+ mutations_from_database.changed_in_place?(attr_name)
end
private
-
- # Returns +true+ if attr_name is changed, +false+ otherwise.
- def changes_include?(attr_name)
- attributes_changed_by_setter.include?(attr_name)
+ def clear_attribute_change(attr_name)
+ mutations_from_database.forget_change(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)
+ def mutations_from_database
+ unless defined?(@mutations_from_database)
+ @mutations_from_database = nil
+ end
+
+ unless defined?(@attributes)
+ @_pseudo_attributes = true
+ @attributes = AttributeSet.new(
+ Hash.new { |h, attr|
+ h[attr] = Attribute.with_cast_value(attr, _clone_attribute(attr), Type.default_value)
+ }
+ )
+ end
+
+ @mutations_from_database ||= ActiveModel::AttributeMutationTracker.new(@attributes)
end
- # Removes current changes and makes them accessible through +previous_changes+.
- def changes_applied # :doc:
- @previously_changed = changes
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
+ def forget_attribute_assignments
+ @attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes)
end
- # Clears all dirty data: current changes and previous changes.
- def clear_changes_information # :doc:
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
+ def mutations_before_last_save
+ @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
end
# Handles <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
- [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
+ [attribute_was(attr), _read_attribute(attr)] if attribute_changed?(attr)
end
# Handles <tt>*_previous_change</tt> for +method_missing+.
def attribute_previous_change(attr)
- previous_changes[attr] if attribute_previously_changed?(attr)
+ mutations_before_last_save.change_to_attribute(attr)
end
# Handles <tt>*_will_change!</tt> for +method_missing+.
def attribute_will_change!(attr)
- return if attribute_changed?(attr)
-
- begin
- value = _read_attribute(attr)
- value = value.duplicable? ? value.clone : value
- rescue TypeError, NoMethodError
+ attr = attr.to_s
+ mutations_from_database.force_change(attr).tap do
+ @attributes[attr] if defined?(@_pseudo_attributes)
end
-
- set_attribute_was(attr, value)
end
# Handles <tt>restore_*!</tt> for +method_missing+.
def restore_attribute!(attr)
if attribute_changed?(attr)
- __send__("#{attr}=", changed_attributes[attr])
+ __send__("#{attr}=", attribute_was(attr))
clear_attribute_changes([attr])
end
end
- # This is necessary because `changed_attributes` might be overridden in
- # other implementations (e.g. in `ActiveRecord`)
- alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
-
- # Force an attribute to have a particular "before" value
- def set_attribute_was(attr, old_value)
- attributes_changed_by_setter[attr] = old_value
+ def _prepare_changes
+ if defined?(@_pseudo_attributes)
+ changed.each do |attr|
+ @attributes.write_from_user(attr, _read_attribute(attr))
+ end
+ end
end
- # Remove changes information for the provided attributes.
- def clear_attribute_changes(attributes) # :doc:
- attributes_changed_by_setter.except!(*attributes)
+ def _clone_attribute(attr)
+ value = _read_attribute(attr)
+ value.duplicable? ? value.clone : value
+ rescue TypeError, NoMethodError
+ value
end
end
end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 971bdd08b1..275e3f1313 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -322,9 +322,13 @@ module ActiveModel
# person.errors.added? :name, :too_long # => false
# person.errors.added? :name, "is too long" # => false
def added?(attribute, message = :invalid, options = {})
- message = message.call if message.respond_to?(:call)
- message = normalize_message(attribute, message, options)
- self[attribute].include? message
+ if message.is_a? Symbol
+ self.details[attribute].map { |e| e[:error] }.include? message
+ else
+ message = message.call if message.respond_to?(:call)
+ message = normalize_message(attribute, message, options)
+ self[attribute].include? message
+ end
end
# Returns all the full error messages in an array.
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
index 39269c159c..3c344fe854 100644
--- a/activemodel/lib/active_model/gem_version.rb
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -10,7 +10,7 @@ module ActiveModel
MAJOR = 5
MINOR = 2
TINY = 0
- PRE = "alpha"
+ PRE = "beta2"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index a09659ad77..dfccd03cd8 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -2,7 +2,7 @@
require "active_support/core_ext/hash/except"
require "active_support/core_ext/module/introspection"
-require "active_support/core_ext/module/remove_method"
+require "active_support/core_ext/module/redefine_method"
module ActiveModel
class Name
@@ -218,7 +218,7 @@ module ActiveModel
# provided method below, or rolling your own is required.
module Naming
def self.extended(base) #:nodoc:
- base.remove_possible_method :model_name
+ base.silence_redefinition_of_method :model_name
base.delegate :model_name, to: :class
end
diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb
index cb603b3d25..1d7a26fff5 100644
--- a/activemodel/lib/active_model/type.rb
+++ b/activemodel/lib/active_model/type.rb
@@ -1,21 +1,21 @@
# frozen_string_literal: true
-require_relative "type/helpers"
-require_relative "type/value"
-
-require_relative "type/big_integer"
-require_relative "type/binary"
-require_relative "type/boolean"
-require_relative "type/date"
-require_relative "type/date_time"
-require_relative "type/decimal"
-require_relative "type/float"
-require_relative "type/immutable_string"
-require_relative "type/integer"
-require_relative "type/string"
-require_relative "type/time"
-
-require_relative "type/registry"
+require "active_model/type/helpers"
+require "active_model/type/value"
+
+require "active_model/type/big_integer"
+require "active_model/type/binary"
+require "active_model/type/boolean"
+require "active_model/type/date"
+require "active_model/type/date_time"
+require "active_model/type/decimal"
+require "active_model/type/float"
+require "active_model/type/immutable_string"
+require "active_model/type/integer"
+require "active_model/type/string"
+require "active_model/type/time"
+
+require "active_model/type/registry"
module ActiveModel
module Type
@@ -24,7 +24,7 @@ module ActiveModel
class << self
attr_accessor :registry # :nodoc:
- # Add a new type to the registry, allowing it to be get through ActiveModel::Type#lookup
+ # Add a new type to the registry, allowing it to be gotten through ActiveModel::Type#lookup
def register(type_name, klass = nil, **options, &block)
registry.register(type_name, klass, **options, &block)
end
@@ -32,6 +32,10 @@ module ActiveModel
def lookup(*args, **kwargs) # :nodoc:
registry.lookup(*args, **kwargs)
end
+
+ def default_value # :nodoc:
+ @default_value ||= Value.new
+ end
end
register(:big_integer, Type::BigInteger)
diff --git a/activemodel/lib/active_model/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb
index d080fcc0f2..89e43bcc5f 100644
--- a/activemodel/lib/active_model/type/big_integer.rb
+++ b/activemodel/lib/active_model/type/big_integer.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "integer"
+require "active_model/type/integer"
module ActiveModel
module Type
diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb
index a4e1427b64..403f0a9e6b 100644
--- a/activemodel/lib/active_model/type/helpers.rb
+++ b/activemodel/lib/active_model/type/helpers.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "helpers/accepts_multiparameter_time"
-require_relative "helpers/numeric"
-require_relative "helpers/mutable"
-require_relative "helpers/time_value"
+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"
diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb
index 6ba2c2a3d2..36f13945b1 100644
--- a/activemodel/lib/active_model/type/string.rb
+++ b/activemodel/lib/active_model/type/string.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "immutable_string"
+require "active_model/type/immutable_string"
module ActiveModel
module Type
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index cdf11d190f..7f14d102dd 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -164,14 +164,14 @@ module ActiveModel
if options.key?(:on)
options = options.dup
+ options[:on] = Array(options[:on])
options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- !(Array(options[:on]) & Array(o.validation_context)).empty?
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- args << options
- set_callback(:validate, *args, &block)
+ set_callback(:validate, *args, options, &block)
end
# List all validators that are being used to validate the model using
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
index 4d0ab2a2fe..887d31ae2a 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -54,15 +54,18 @@ module ActiveModel
# person.valid? # => true
# person.name # => "bob"
def before_validation(*args, &block)
- options = args.last
- if options.is_a?(Hash) && options[:on]
- options[:if] = Array(options[:if])
+ options = args.extract_options!
+
+ if options.key?(:on)
+ options = options.dup
options[:on] = Array(options[:on])
+ options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- options[:on].include? o.validation_context
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- set_callback(:validation, :before, *args, &block)
+
+ set_callback(:validation, :before, *args, options, &block)
end
# Defines a callback that will get called right after validation.
@@ -93,15 +96,18 @@ module ActiveModel
# person.status # => true
def after_validation(*args, &block)
options = args.extract_options!
+ options = options.dup
options[:prepend] = true
- options[:if] = Array(options[:if])
- if options[:on]
+
+ if options.key?(:on)
options[:on] = Array(options[:on])
+ options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- options[:on].include? o.validation_context
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- set_callback(:validation, :after, *(args << options), &block)
+
+ set_callback(:validation, :after, *args, options, &block)
end
end
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index 0abec56b68..1b5d5b09ab 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -9,7 +9,7 @@ module ActiveModel
end
def validate_each(record, attribute, value)
- if (confirmed = record.send("#{attribute}_confirmation"))
+ unless (confirmed = record.send("#{attribute}_confirmation")).nil?
unless confirmation_value_equal?(record, attribute, value, confirmed)
human_attribute_name = record.class.human_attribute_name(attribute)
record.errors.add(:"#{attribute}_confirmation", :confirmation, options.except(:case_sensitive).merge!(attribute: human_attribute_name))
diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb
index a6cbfcc813..3be7ab6ba8 100644
--- a/activemodel/lib/active_model/validations/exclusion.rb
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "clusivity"
+require "active_model/validations/clusivity"
module ActiveModel
module Validations
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
index 00e27b528a..3104e7e329 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "clusivity"
+require "active_model/validations/clusivity"
module ActiveModel
module Validations
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index d1a4197286..d6c80b2c5d 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -31,8 +31,8 @@ module ActiveModel
keys.each do |key|
value = options[key]
- unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY
- raise ArgumentError, ":#{key} must be a nonnegative Integer or Infinity"
+ unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc)
+ raise ArgumentError, ":#{key} must be a nonnegative Integer, Infinity, Symbol, or Proc"
end
end
end
@@ -45,6 +45,12 @@ module ActiveModel
next unless check_value = options[key]
if !value.nil? || skip_nil_check?(key)
+ case check_value
+ when Proc
+ check_value = check_value.call(record)
+ when Symbol
+ check_value = record.send(check_value)
+ end
next if value_length.send(validity_check, check_value)
end
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
index 43d9f82d9f..e28e7e9219 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -154,7 +154,7 @@ module ActiveModel
# When creating custom validators, it might be useful to be able to specify
# additional default keys. This can be done by overwriting this method.
def _validates_default_keys
- [:if, :unless, :on, :allow_blank, :allow_nil , :strict]
+ [:if, :unless, :on, :allow_blank, :allow_nil, :strict]
end
def _parse_validates_options(options)