Move Attribute and AttributeSet to ActiveModel
Use these to back the attributes API. Stop automatically including ActiveModel::Dirty in ActiveModel::Attributes, and make it optional.
This commit is contained in:
parent
dac7c8844b
commit
c3675f50d2
|
@ -30,6 +30,8 @@ require "active_model/version"
|
||||||
module ActiveModel
|
module ActiveModel
|
||||||
extend ActiveSupport::Autoload
|
extend ActiveSupport::Autoload
|
||||||
|
|
||||||
|
autoload :Attribute
|
||||||
|
autoload :Attributes
|
||||||
autoload :AttributeAssignment
|
autoload :AttributeAssignment
|
||||||
autoload :AttributeMethods
|
autoload :AttributeMethods
|
||||||
autoload :BlockValidator, "active_model/validator"
|
autoload :BlockValidator, "active_model/validator"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveModel
|
||||||
class Attribute # :nodoc:
|
class Attribute # :nodoc:
|
||||||
class << self
|
class << self
|
||||||
def from_database(name, value, type)
|
def from_database(name, value, type)
|
||||||
|
@ -130,8 +130,6 @@ module ActiveRecord
|
||||||
coder["value"] = value if defined?(@value)
|
coder["value"] = value if defined?(@value)
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO Change this to private once we've dropped Ruby 2.2 support.
|
|
||||||
# Workaround for Ruby 2.2 "private attribute?" warning.
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
attr_reader :original_attribute
|
attr_reader :original_attribute
|
||||||
|
@ -237,6 +235,7 @@ module ActiveRecord
|
||||||
self.class.new(name, type)
|
self.class.new(name, type)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
|
private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -1,8 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "active_record/attribute"
|
require "active_model/attribute"
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveModel
|
||||||
class Attribute # :nodoc:
|
class Attribute # :nodoc:
|
||||||
class UserProvidedDefault < FromUser # :nodoc:
|
class UserProvidedDefault < FromUser # :nodoc:
|
||||||
def initialize(name, value, type, database_default)
|
def initialize(name, value, type, database_default)
|
||||||
|
@ -22,8 +22,6 @@ module ActiveRecord
|
||||||
self.class.new(name, user_provided_value, type, original_attribute)
|
self.class.new(name, user_provided_value, type, original_attribute)
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO Change this to private once we've dropped Ruby 2.2 support.
|
|
||||||
# Workaround for Ruby 2.2 "private attribute?" warning.
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
attr_reader :user_provided_value
|
attr_reader :user_provided_value
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveModel
|
||||||
class AttributeMutationTracker # :nodoc:
|
class AttributeMutationTracker # :nodoc:
|
||||||
OPTION_NOT_GIVEN = Object.new
|
OPTION_NOT_GIVEN = Object.new
|
||||||
|
|
||||||
|
@ -107,5 +107,8 @@ module ActiveRecord
|
||||||
|
|
||||||
def original_value(*)
|
def original_value(*)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def force_change(*)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -1,9 +1,9 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "active_record/attribute_set/builder"
|
require "active_model/attribute_set/builder"
|
||||||
require "active_record/attribute_set/yaml_encoder"
|
require "active_model/attribute_set/yaml_encoder"
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveModel
|
||||||
class AttributeSet # :nodoc:
|
class AttributeSet # :nodoc:
|
||||||
delegate :each_value, :fetch, to: :attributes
|
delegate :each_value, :fetch, to: :attributes
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "active_record/attribute"
|
require "active_model/attribute"
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveModel
|
||||||
class AttributeSet # :nodoc:
|
class AttributeSet # :nodoc:
|
||||||
class Builder # :nodoc:
|
class Builder # :nodoc:
|
||||||
attr_reader :types, :always_initialized, :default
|
attr_reader :types, :always_initialized, :default
|
||||||
|
@ -92,8 +92,6 @@ module ActiveRecord
|
||||||
@materialized = true
|
@materialized = true
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO Change this to private once we've dropped Ruby 2.2 support.
|
|
||||||
# Workaround for Ruby 2.2 "private attribute?" warning.
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
attr_reader :types, :values, :additional_types, :delegate_hash, :default
|
attr_reader :types, :values, :additional_types, :delegate_hash, :default
|
|
@ -1,9 +1,9 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveModel
|
||||||
class AttributeSet
|
class AttributeSet
|
||||||
# Attempts to do more intelligent YAML dumping of an
|
# Attempts to do more intelligent YAML dumping of an
|
||||||
# ActiveRecord::AttributeSet to reduce the size of the resulting string
|
# ActiveModel::AttributeSet to reduce the size of the resulting string
|
||||||
class YAMLEncoder # :nodoc:
|
class YAMLEncoder # :nodoc:
|
||||||
def initialize(default_types)
|
def initialize(default_types)
|
||||||
@default_types = default_types
|
@default_types = default_types
|
||||||
|
@ -33,8 +33,6 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO Change this to private once we've dropped Ruby 2.2 support.
|
|
||||||
# Workaround for Ruby 2.2 "private attribute?" warning.
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
attr_reader :default_types
|
attr_reader :default_types
|
|
@ -1,24 +1,30 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "active_support/core_ext/object/deep_dup"
|
||||||
require "active_model/type"
|
require "active_model/type"
|
||||||
|
require "active_model/attribute_set"
|
||||||
|
require "active_model/attribute/user_provided_default"
|
||||||
|
|
||||||
module ActiveModel
|
module ActiveModel
|
||||||
module Attributes #:nodoc:
|
module Attributes #:nodoc:
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
include ActiveModel::AttributeMethods
|
include ActiveModel::AttributeMethods
|
||||||
include ActiveModel::Dirty
|
|
||||||
|
|
||||||
included do
|
included do
|
||||||
attribute_method_suffix "="
|
attribute_method_suffix "="
|
||||||
class_attribute :attribute_types, :_default_attributes, instance_accessor: false
|
class_attribute :attribute_types, :_default_attributes, instance_accessor: false
|
||||||
self.attribute_types = {}
|
self.attribute_types = Hash.new(Type.default_value)
|
||||||
self._default_attributes = {}
|
self._default_attributes = AttributeSet.new({})
|
||||||
end
|
end
|
||||||
|
|
||||||
module ClassMethods
|
module ClassMethods
|
||||||
def attribute(name, cast_type = Type::Value.new, **options)
|
def attribute(name, type = Type::Value.new, **options)
|
||||||
self.attribute_types = attribute_types.merge(name.to_s => cast_type)
|
name = name.to_s
|
||||||
self._default_attributes = _default_attributes.merge(name.to_s => options[:default])
|
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_methods(name)
|
define_attribute_methods(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -37,11 +43,29 @@ module ActiveModel
|
||||||
undef_method :__temp__#{safe_name}=
|
undef_method :__temp__#{safe_name}=
|
||||||
STR
|
STR
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def initialize(*)
|
def initialize(*)
|
||||||
|
@attributes = self.class._default_attributes.deep_dup
|
||||||
super
|
super
|
||||||
clear_changes_information
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -53,21 +77,17 @@ module ActiveModel
|
||||||
attr_name.to_s
|
attr_name.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
cast_type = self.class.attribute_types[name]
|
@attributes.write_from_user(attr_name.to_s, value)
|
||||||
|
value
|
||||||
deserialized_value = ActiveModel::Type.lookup(cast_type).cast(value)
|
|
||||||
attribute_will_change!(name) unless deserialized_value == attribute(name)
|
|
||||||
instance_variable_set("@#{name}", deserialized_value)
|
|
||||||
deserialized_value
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def attribute(name)
|
def attribute(attr_name)
|
||||||
if instance_variable_defined?("@#{name}")
|
name = if self.class.attribute_alias?(attr_name)
|
||||||
instance_variable_get("@#{name}")
|
self.class.attribute_alias(attr_name).to_s
|
||||||
else
|
else
|
||||||
default = self.class._default_attributes[name]
|
attr_name.to_s
|
||||||
default.respond_to?(:call) ? default.call : default
|
|
||||||
end
|
end
|
||||||
|
@attributes.fetch_value(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Handle *= for method_missing.
|
# Handle *= for method_missing.
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
require "active_support/hash_with_indifferent_access"
|
require "active_support/hash_with_indifferent_access"
|
||||||
require "active_support/core_ext/object/duplicable"
|
require "active_support/core_ext/object/duplicable"
|
||||||
|
require "active_model/attribute_mutation_tracker"
|
||||||
|
|
||||||
module ActiveModel
|
module ActiveModel
|
||||||
# == Active \Model \Dirty
|
# == Active \Model \Dirty
|
||||||
|
@ -130,6 +131,24 @@ module ActiveModel
|
||||||
attribute_method_affix prefix: "restore_", suffix: "!"
|
attribute_method_affix prefix: "restore_", suffix: "!"
|
||||||
end
|
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:
|
||||||
|
@previously_changed = changes
|
||||||
|
@mutations_before_last_save = mutations_from_database
|
||||||
|
@attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
|
||||||
|
forget_attribute_assignments
|
||||||
|
@mutations_from_database = nil
|
||||||
|
end
|
||||||
|
|
||||||
# Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
|
# Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
|
||||||
#
|
#
|
||||||
# person.changed? # => false
|
# person.changed? # => false
|
||||||
|
@ -148,36 +167,6 @@ module ActiveModel
|
||||||
changed_attributes.keys
|
changed_attributes.keys
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a hash of changed attributes indicating their original
|
|
||||||
# and new values like <tt>attr => [original value, new value]</tt>.
|
|
||||||
#
|
|
||||||
# person.changes # => {}
|
|
||||||
# person.name = 'bob'
|
|
||||||
# person.changes # => { "name" => ["bill", "bob"] }
|
|
||||||
def changes
|
|
||||||
ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns a hash of attributes that were changed before the model was saved.
|
|
||||||
#
|
|
||||||
# person.name # => "bob"
|
|
||||||
# person.name = 'robert'
|
|
||||||
# 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+.
|
# Handles <tt>*_changed?</tt> for +method_missing+.
|
||||||
def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
|
def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
|
||||||
!!changes_include?(attr) &&
|
!!changes_include?(attr) &&
|
||||||
|
@ -200,11 +189,103 @@ module ActiveModel
|
||||||
attributes.each { |attr| restore_attribute! attr }
|
attributes.each { |attr| restore_attribute! attr }
|
||||||
end
|
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
|
||||||
|
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
|
||||||
|
# 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
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a hash of changed attributes indicating their original
|
||||||
|
# and new values like <tt>attr => [original value, new value]</tt>.
|
||||||
|
#
|
||||||
|
# person.changes # => {}
|
||||||
|
# person.name = 'bob'
|
||||||
|
# person.changes # => { "name" => ["bill", "bob"] }
|
||||||
|
def changes
|
||||||
|
cache_changed_attributes do
|
||||||
|
ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a hash of attributes that were changed before the model was saved.
|
||||||
|
#
|
||||||
|
# person.name # => "bob"
|
||||||
|
# person.name = 'robert'
|
||||||
|
# person.save
|
||||||
|
# person.previous_changes # => {"name" => ["bob", "robert"]}
|
||||||
|
def previous_changes
|
||||||
|
@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
|
||||||
|
@previously_changed.merge(mutations_before_last_save.changes)
|
||||||
|
end
|
||||||
|
|
||||||
|
def attribute_changed_in_place?(attr_name) # :nodoc:
|
||||||
|
mutations_from_database.changed_in_place?(attr_name)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def clear_attribute_change(attr_name)
|
||||||
|
mutations_from_database.forget_change(attr_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def mutations_from_database
|
||||||
|
unless defined?(@mutations_from_database)
|
||||||
|
@mutations_from_database = nil
|
||||||
|
end
|
||||||
|
@mutations_from_database ||= if @attributes
|
||||||
|
ActiveModel::AttributeMutationTracker.new(@attributes)
|
||||||
|
else
|
||||||
|
NullMutationTracker.instance
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def forget_attribute_assignments
|
||||||
|
@attributes = @attributes.map(&:forgetting_assignment) if @attributes
|
||||||
|
end
|
||||||
|
|
||||||
|
def mutations_before_last_save
|
||||||
|
@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.
|
# Returns +true+ if attr_name is changed, +false+ otherwise.
|
||||||
def changes_include?(attr_name)
|
def changes_include?(attr_name)
|
||||||
attributes_changed_by_setter.include?(attr_name)
|
attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
|
||||||
end
|
end
|
||||||
alias attribute_changed_by_setter? changes_include?
|
alias attribute_changed_by_setter? changes_include?
|
||||||
|
|
||||||
|
@ -214,18 +295,6 @@ module ActiveModel
|
||||||
previous_changes.include?(attr_name)
|
previous_changes.include?(attr_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Removes current changes and makes them accessible through +previous_changes+.
|
|
||||||
def changes_applied # :doc:
|
|
||||||
@previously_changed = changes
|
|
||||||
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
# Handles <tt>*_change</tt> for +method_missing+.
|
# Handles <tt>*_change</tt> for +method_missing+.
|
||||||
def attribute_change(attr)
|
def attribute_change(attr)
|
||||||
[changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
|
[changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
|
||||||
|
@ -238,8 +307,7 @@ module ActiveModel
|
||||||
|
|
||||||
# Handles <tt>*_will_change!</tt> for +method_missing+.
|
# Handles <tt>*_will_change!</tt> for +method_missing+.
|
||||||
def attribute_will_change!(attr)
|
def attribute_will_change!(attr)
|
||||||
return if attribute_changed?(attr)
|
unless attribute_changed?(attr)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
value = _read_attribute(attr)
|
value = _read_attribute(attr)
|
||||||
value = value.duplicable? ? value.clone : value
|
value = value.duplicable? ? value.clone : value
|
||||||
|
@ -248,6 +316,8 @@ module ActiveModel
|
||||||
|
|
||||||
set_attribute_was(attr, value)
|
set_attribute_was(attr, value)
|
||||||
end
|
end
|
||||||
|
mutations_from_database.force_change(attr)
|
||||||
|
end
|
||||||
|
|
||||||
# Handles <tt>restore_*!</tt> for +method_missing+.
|
# Handles <tt>restore_*!</tt> for +method_missing+.
|
||||||
def restore_attribute!(attr)
|
def restore_attribute!(attr)
|
||||||
|
@ -257,18 +327,13 @@ module ActiveModel
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# This is necessary because `changed_attributes` might be overridden in
|
def attributes_changed_by_setter
|
||||||
# other implementations (e.g. in `ActiveRecord`)
|
@attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
|
||||||
alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
|
end
|
||||||
|
|
||||||
# Force an attribute to have a particular "before" value
|
# Force an attribute to have a particular "before" value
|
||||||
def set_attribute_was(attr, old_value)
|
def set_attribute_was(attr, old_value)
|
||||||
attributes_changed_by_setter[attr] = old_value
|
attributes_changed_by_setter[attr] = old_value
|
||||||
end
|
end
|
||||||
|
|
||||||
# Remove changes information for the provided attributes.
|
|
||||||
def clear_attribute_changes(attributes) # :doc:
|
|
||||||
attributes_changed_by_setter.except!(*attributes)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,6 +32,10 @@ module ActiveModel
|
||||||
def lookup(*args, **kwargs) # :nodoc:
|
def lookup(*args, **kwargs) # :nodoc:
|
||||||
registry.lookup(*args, **kwargs)
|
registry.lookup(*args, **kwargs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def default_value # :nodoc:
|
||||||
|
@default_value ||= Value.new
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
register(:big_integer, Type::BigInteger)
|
register(:big_integer, Type::BigInteger)
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
require "cases/helper"
|
require "cases/helper"
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveModel
|
||||||
class AttributeSetTest < ActiveRecord::TestCase
|
class AttributeSetTest < ActiveModel::TestCase
|
||||||
test "building a new set from raw attributes" do
|
test "building a new set from raw attributes" do
|
||||||
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
|
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
|
||||||
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
|
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
require "cases/helper"
|
require "cases/helper"
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveModel
|
||||||
class AttributeTest < ActiveRecord::TestCase
|
class AttributeTest < ActiveModel::TestCase
|
||||||
setup do
|
setup do
|
||||||
@type = Minitest::Mock.new
|
@type = Minitest::Mock.new
|
||||||
end
|
end
|
|
@ -1,12 +1,12 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "cases/helper"
|
require "cases/helper"
|
||||||
require "active_model/attributes"
|
|
||||||
|
|
||||||
class AttributesDirtyTest < ActiveModel::TestCase
|
class AttributesDirtyTest < ActiveModel::TestCase
|
||||||
class DirtyModel
|
class DirtyModel
|
||||||
include ActiveModel::Model
|
include ActiveModel::Model
|
||||||
include ActiveModel::Attributes
|
include ActiveModel::Attributes
|
||||||
|
include ActiveModel::Dirty
|
||||||
attribute :name, :string
|
attribute :name, :string
|
||||||
attribute :color, :string
|
attribute :color, :string
|
||||||
attribute :size, :integer
|
attribute :size, :integer
|
||||||
|
@ -69,12 +69,10 @@ class AttributesDirtyTest < ActiveModel::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "attribute mutation" do
|
test "attribute mutation" do
|
||||||
@model.instance_variable_set("@name", "Yam".dup)
|
@model.name = "Yam"
|
||||||
|
@model.save
|
||||||
assert !@model.name_changed?
|
assert !@model.name_changed?
|
||||||
@model.name.replace("Hadad")
|
@model.name.replace("Hadad")
|
||||||
assert !@model.name_changed?
|
|
||||||
@model.name_will_change!
|
|
||||||
@model.name.replace("Baal")
|
|
||||||
assert @model.name_changed?
|
assert @model.name_changed?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -190,4 +188,18 @@ class AttributesDirtyTest < ActiveModel::TestCase
|
||||||
assert_equal "Dmitry", @model.name
|
assert_equal "Dmitry", @model.name
|
||||||
assert_equal "White", @model.color
|
assert_equal "White", @model.color
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "changing the attribute reports a change only when the cast value changes" do
|
||||||
|
@model.size = "2.3"
|
||||||
|
@model.save
|
||||||
|
@model.size = "2.1"
|
||||||
|
|
||||||
|
assert_equal false, @model.changed?
|
||||||
|
|
||||||
|
@model.size = "5.1"
|
||||||
|
|
||||||
|
assert_equal true, @model.changed?
|
||||||
|
assert_equal true, @model.size_changed?
|
||||||
|
assert_equal({ "size" => [2, 5] }, @model.changes)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "cases/helper"
|
require "cases/helper"
|
||||||
require "active_model/attributes"
|
|
||||||
|
|
||||||
module ActiveModel
|
module ActiveModel
|
||||||
class AttributesTest < ActiveModel::TestCase
|
class AttributesTest < ActiveModel::TestCase
|
||||||
|
@ -13,7 +12,7 @@ module ActiveModel
|
||||||
attribute :string_field, :string
|
attribute :string_field, :string
|
||||||
attribute :decimal_field, :decimal
|
attribute :decimal_field, :decimal
|
||||||
attribute :string_with_default, :string, default: "default string"
|
attribute :string_with_default, :string, default: "default string"
|
||||||
attribute :date_field, :string, default: -> { Date.new(2016, 1, 1) }
|
attribute :date_field, :date, default: -> { Date.new(2016, 1, 1) }
|
||||||
attribute :boolean_field, :boolean
|
attribute :boolean_field, :boolean
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -48,31 +47,6 @@ module ActiveModel
|
||||||
assert_equal true, data.boolean_field
|
assert_equal true, data.boolean_field
|
||||||
end
|
end
|
||||||
|
|
||||||
test "dirty" do
|
|
||||||
data = ModelForAttributesTest.new(
|
|
||||||
integer_field: "2.3",
|
|
||||||
string_field: "Rails FTW",
|
|
||||||
decimal_field: "12.3",
|
|
||||||
boolean_field: "0"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert_equal false, data.changed?
|
|
||||||
|
|
||||||
data.integer_field = "2.1"
|
|
||||||
|
|
||||||
assert_equal false, data.changed?
|
|
||||||
|
|
||||||
data.string_with_default = "default string"
|
|
||||||
|
|
||||||
assert_equal false, data.changed?
|
|
||||||
|
|
||||||
data.integer_field = "5.1"
|
|
||||||
|
|
||||||
assert_equal true, data.changed?
|
|
||||||
assert_equal true, data.integer_field_changed?
|
|
||||||
assert_equal({ "integer_field" => [2, 5] }, data.changes)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "nonexistent attribute" do
|
test "nonexistent attribute" do
|
||||||
assert_raise ActiveModel::UnknownAttributeError do
|
assert_raise ActiveModel::UnknownAttributeError do
|
||||||
ModelForAttributesTest.new(nonexistent: "nonexistent")
|
ModelForAttributesTest.new(nonexistent: "nonexistent")
|
||||||
|
|
|
@ -219,4 +219,8 @@ class DirtyTest < ActiveModel::TestCase
|
||||||
assert_equal "Dmitry", @model.name
|
assert_equal "Dmitry", @model.name
|
||||||
assert_equal "White", @model.color
|
assert_equal "White", @model.color
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "model can be dup-ed without Attributes" do
|
||||||
|
assert @model.dup
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,14 +27,14 @@ require "active_support"
|
||||||
require "active_support/rails"
|
require "active_support/rails"
|
||||||
require "active_model"
|
require "active_model"
|
||||||
require "arel"
|
require "arel"
|
||||||
|
require "yaml"
|
||||||
|
|
||||||
require "active_record/version"
|
require "active_record/version"
|
||||||
require "active_record/attribute_set"
|
require "active_model/attribute_set"
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveRecord
|
||||||
extend ActiveSupport::Autoload
|
extend ActiveSupport::Autoload
|
||||||
|
|
||||||
autoload :Attribute
|
|
||||||
autoload :Base
|
autoload :Base
|
||||||
autoload :Callbacks
|
autoload :Callbacks
|
||||||
autoload :Core
|
autoload :Core
|
||||||
|
@ -181,3 +181,7 @@ end
|
||||||
ActiveSupport.on_load(:i18n) do
|
ActiveSupport.on_load(:i18n) do
|
||||||
I18n.load_path << File.expand_path("active_record/locale/en.yml", __dir__)
|
I18n.load_path << File.expand_path("active_record/locale/en.yml", __dir__)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
YAML.load_tags["!ruby/object:ActiveRecord::AttributeSet"] = "ActiveModel::AttributeSet"
|
||||||
|
YAML.load_tags["!ruby/object:ActiveRecord::Attribute::FromDatabase"] = "ActiveModel::Attribute::FromDatabase"
|
||||||
|
YAML.load_tags["!ruby/object:ActiveRecord::LazyAttributeHash"] = "ActiveModel::LazyAttributeHash"
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "active_support/core_ext/module/attribute_accessors"
|
require "active_support/core_ext/module/attribute_accessors"
|
||||||
require "active_record/attribute_mutation_tracker"
|
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveRecord
|
||||||
module AttributeMethods
|
module AttributeMethods
|
||||||
|
@ -33,63 +32,11 @@ module ActiveRecord
|
||||||
# <tt>reload</tt> the record and clears changed attributes.
|
# <tt>reload</tt> the record and clears changed attributes.
|
||||||
def reload(*)
|
def reload(*)
|
||||||
super.tap do
|
super.tap do
|
||||||
|
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
|
||||||
@mutations_before_last_save = nil
|
@mutations_before_last_save = nil
|
||||||
@mutations_from_database = nil
|
@attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
|
||||||
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize_dup(other) # :nodoc:
|
|
||||||
super
|
|
||||||
@attributes = self.class._default_attributes.map do |attr|
|
|
||||||
attr.with_value_from_user(@attributes.fetch_value(attr.name))
|
|
||||||
end
|
|
||||||
@mutations_from_database = nil
|
@mutations_from_database = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def changes_applied # :nodoc:
|
|
||||||
@mutations_before_last_save = mutations_from_database
|
|
||||||
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
|
||||||
forget_attribute_assignments
|
|
||||||
@mutations_from_database = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_changes_information # :nodoc:
|
|
||||||
@mutations_before_last_save = nil
|
|
||||||
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
|
||||||
forget_attribute_assignments
|
|
||||||
@mutations_from_database = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_attribute_changes(attr_names) # :nodoc:
|
|
||||||
super
|
|
||||||
attr_names.each do |attr_name|
|
|
||||||
clear_attribute_change(attr_name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def changed_attributes # :nodoc:
|
|
||||||
# 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
|
|
||||||
super.reverse_merge(mutations_from_database.changed_values).freeze
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def changes # :nodoc:
|
|
||||||
cache_changed_attributes do
|
|
||||||
super
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def previous_changes # :nodoc:
|
|
||||||
mutations_before_last_save.changes
|
|
||||||
end
|
|
||||||
|
|
||||||
def attribute_changed_in_place?(attr_name) # :nodoc:
|
|
||||||
mutations_from_database.changed_in_place?(attr_name)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Did this attribute change when we last saved? This method can be invoked
|
# Did this attribute change when we last saved? This method can be invoked
|
||||||
|
@ -182,26 +129,6 @@ module ActiveRecord
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
def mutations_from_database
|
|
||||||
unless defined?(@mutations_from_database)
|
|
||||||
@mutations_from_database = nil
|
|
||||||
end
|
|
||||||
@mutations_from_database ||= AttributeMutationTracker.new(@attributes)
|
|
||||||
end
|
|
||||||
|
|
||||||
def changes_include?(attr_name)
|
|
||||||
super || mutations_from_database.changed?(attr_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_attribute_change(attr_name)
|
|
||||||
mutations_from_database.forget_change(attr_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def attribute_will_change!(attr_name)
|
|
||||||
super
|
|
||||||
mutations_from_database.force_change(attr_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def _update_record(*)
|
def _update_record(*)
|
||||||
partial_writes? ? super(keys_for_partial_write) : super
|
partial_writes? ? super(keys_for_partial_write) : super
|
||||||
end
|
end
|
||||||
|
@ -213,25 +140,6 @@ module ActiveRecord
|
||||||
def keys_for_partial_write
|
def keys_for_partial_write
|
||||||
changed_attribute_names_to_save & self.class.column_names
|
changed_attribute_names_to_save & self.class.column_names
|
||||||
end
|
end
|
||||||
|
|
||||||
def forget_attribute_assignments
|
|
||||||
@attributes = @attributes.map(&:forgetting_assignment)
|
|
||||||
end
|
|
||||||
|
|
||||||
def mutations_before_last_save
|
|
||||||
@mutations_before_last_save ||= 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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "active_record/attribute/user_provided_default"
|
require "active_model/attribute/user_provided_default"
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveRecord
|
||||||
# See ActiveRecord::Attributes::ClassMethods for documentation
|
# See ActiveRecord::Attributes::ClassMethods for documentation
|
||||||
|
@ -250,14 +250,14 @@ module ActiveRecord
|
||||||
if value == NO_DEFAULT_PROVIDED
|
if value == NO_DEFAULT_PROVIDED
|
||||||
default_attribute = _default_attributes[name].with_type(type)
|
default_attribute = _default_attributes[name].with_type(type)
|
||||||
elsif from_user
|
elsif from_user
|
||||||
default_attribute = Attribute::UserProvidedDefault.new(
|
default_attribute = ActiveModel::Attribute::UserProvidedDefault.new(
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
type,
|
type,
|
||||||
_default_attributes.fetch(name.to_s) { nil },
|
_default_attributes.fetch(name.to_s) { nil },
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
default_attribute = Attribute.from_database(name, value, type)
|
default_attribute = ActiveModel::Attribute.from_database(name, value, type)
|
||||||
end
|
end
|
||||||
_default_attributes[name] = default_attribute
|
_default_attributes[name] = default_attribute
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,7 @@ module ActiveRecord
|
||||||
case coder["active_record_yaml_version"]
|
case coder["active_record_yaml_version"]
|
||||||
when 1, 2 then coder
|
when 1, 2 then coder
|
||||||
else
|
else
|
||||||
if coder["attributes"].is_a?(AttributeSet)
|
if coder["attributes"].is_a?(ActiveModel::AttributeSet)
|
||||||
Rails420.convert(klass, coder)
|
Rails420.convert(klass, coder)
|
||||||
else
|
else
|
||||||
Rails41.convert(klass, coder)
|
Rails41.convert(klass, coder)
|
||||||
|
|
|
@ -323,7 +323,7 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def attributes_builder # :nodoc:
|
def attributes_builder # :nodoc:
|
||||||
@attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) do |name|
|
@attributes_builder ||= ActiveModel::AttributeSet::Builder.new(attribute_types, primary_key) do |name|
|
||||||
unless columns_hash.key?(name)
|
unless columns_hash.key?(name)
|
||||||
_default_attributes[name].dup
|
_default_attributes[name].dup
|
||||||
end
|
end
|
||||||
|
@ -346,7 +346,7 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def yaml_encoder # :nodoc:
|
def yaml_encoder # :nodoc:
|
||||||
@yaml_encoder ||= AttributeSet::YAMLEncoder.new(attribute_types)
|
@yaml_encoder ||= ActiveModel::AttributeSet::YAMLEncoder.new(attribute_types)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the type of the attribute with the given name, after applying
|
# Returns the type of the attribute with the given name, after applying
|
||||||
|
@ -376,7 +376,7 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def _default_attributes # :nodoc:
|
def _default_attributes # :nodoc:
|
||||||
@default_attributes ||= AttributeSet.new({})
|
@default_attributes ||= ActiveModel::AttributeSet.new({})
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns an array of column names as strings.
|
# Returns an array of column names as strings.
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "active_record/attribute"
|
require "active_model/attribute"
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveRecord
|
||||||
class Relation
|
class Relation
|
||||||
class QueryAttribute < Attribute # :nodoc:
|
class QueryAttribute < ActiveModel::Attribute # :nodoc:
|
||||||
def type_cast(value)
|
def type_cast(value)
|
||||||
value
|
value
|
||||||
end
|
end
|
||||||
|
|
|
@ -930,7 +930,7 @@ module ActiveRecord
|
||||||
arel.where(where_clause.ast) unless where_clause.empty?
|
arel.where(where_clause.ast) unless where_clause.empty?
|
||||||
arel.having(having_clause.ast) unless having_clause.empty?
|
arel.having(having_clause.ast) unless having_clause.empty?
|
||||||
if limit_value
|
if limit_value
|
||||||
limit_attribute = Attribute.with_cast_value(
|
limit_attribute = ActiveModel::Attribute.with_cast_value(
|
||||||
"LIMIT".freeze,
|
"LIMIT".freeze,
|
||||||
connection.sanitize_limit(limit_value),
|
connection.sanitize_limit(limit_value),
|
||||||
Type.default_value,
|
Type.default_value,
|
||||||
|
@ -938,7 +938,7 @@ module ActiveRecord
|
||||||
arel.take(Arel::Nodes::BindParam.new(limit_attribute))
|
arel.take(Arel::Nodes::BindParam.new(limit_attribute))
|
||||||
end
|
end
|
||||||
if offset_value
|
if offset_value
|
||||||
offset_attribute = Attribute.with_cast_value(
|
offset_attribute = ActiveModel::Attribute.with_cast_value(
|
||||||
"OFFSET".freeze,
|
"OFFSET".freeze,
|
||||||
offset_value.to_i,
|
offset_value.to_i,
|
||||||
Type.default_value,
|
Type.default_value,
|
||||||
|
|
Loading…
Reference in New Issue