From 7e9ded512dd58a923a3be038843708dbba4215f6 Mon Sep 17 00:00:00 2001 From: Lisa Ugray Date: Thu, 12 Oct 2017 14:13:45 -0400 Subject: Start bringing attributes API to AM This is the first PR of a WIP to bring the attributes API to ActiveModel. It is not yet ready for public API. The `attributes_dirty_test.rb` file was created based on `dirty_test.rb`, and the simplifications in the diff do much to motivate this change. ``` diff activemodel/test/cases/dirty_test.rb activemodel/test/cases/attributes_dirty_test.rb 3a4 > require "active_model/attributes" 5c6 < class DirtyTest < ActiveModel::TestCase --- > class AttributesDirtyTest < ActiveModel::TestCase 7,41c8,12 < include ActiveModel::Dirty < define_attribute_methods :name, :color, :size < < def initialize < @name = nil < @color = nil < @size = nil < end < < def name < @name < end < < def name=(val) < name_will_change! < @name = val < end < < def color < @color < end < < def color=(val) < color_will_change! unless val == @color < @color = val < end < < def size < @size < end < < def size=(val) < attribute_will_change!(:size) unless val == @size < @size = val < end --- > include ActiveModel::Model > include ActiveModel::Attributes > attribute :name, :string > attribute :color, :string > attribute :size, :integer ``` --- activemodel/lib/active_model/attributes.rb | 89 ++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 activemodel/lib/active_model/attributes.rb (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb new file mode 100644 index 0000000000..3e34d3b83a --- /dev/null +++ b/activemodel/lib/active_model/attributes.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require "active_model/type" + +module ActiveModel + module Attributes #:nodoc: + extend ActiveSupport::Concern + include ActiveModel::AttributeMethods + include ActiveModel::Dirty + + included do + attribute_method_suffix "=" + class_attribute :attribute_types, :_default_attributes, instance_accessor: false + self.attribute_types = {} + self._default_attributes = {} + end + + module ClassMethods + def attribute(name, cast_type = Type::Value.new, **options) + self.attribute_types = attribute_types.merge(name.to_s => cast_type) + self._default_attributes = _default_attributes.merge(name.to_s => options[:default]) + define_attribute_methods(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 + end + + def initialize(*) + super + clear_changes_information + 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 + + cast_type = self.class.attribute_types[name] + + 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 + + def attribute(name) + if instance_variable_defined?("@#{name}") + instance_variable_get("@#{name}") + else + default = self.class._default_attributes[name] + default.respond_to?(:call) ? default.call : default + end + 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 -- cgit v1.2.3 From 8c2ba84d6301b429bd6d68d7b8fd24f86926dabd Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Sat, 21 Oct 2017 22:15:16 +0900 Subject: [Active Model] require => require_relative This basically reverts ee5cfc01a5797f854c8441539b0cae326a81b963 --- activemodel/lib/active_model.rb | 2 +- activemodel/lib/active_model/type.rb | 32 +++++++++++----------- activemodel/lib/active_model/type/big_integer.rb | 2 +- activemodel/lib/active_model/type/helpers.rb | 8 +++--- activemodel/lib/active_model/type/string.rb | 2 +- .../lib/active_model/validations/exclusion.rb | 2 +- .../lib/active_model/validations/inclusion.rb | 2 +- 7 files changed, 25 insertions(+), 25 deletions(-) (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index dfd9be34a9..a97d7989be 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -25,7 +25,7 @@ require "active_support" require "active_support/rails" -require_relative "active_model/version" +require "active_model/version" module ActiveModel extend ActiveSupport::Autoload diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb index cb603b3d25..b0ed67f28e 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 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/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 -- cgit v1.2.3 From b8b089ef11300fab4625011b942ee4a1fbbdbbbd Mon Sep 17 00:00:00 2001 From: Matt Rohrer Date: Thu, 21 Sep 2017 15:39:49 +0200 Subject: Allow passing a Proc or Symbol as an argument to length validator values This brings the Length validator in line with the Numericality validator, which currently supports Proc & Symbol arguments --- activemodel/lib/active_model/validations/length.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'activemodel/lib') 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 -- cgit v1.2.3 From 7308991630af3fb8a2a7e2cde9fd322316153ec2 Mon Sep 17 00:00:00 2001 From: bogdanvlviv Date: Sun, 5 Nov 2017 19:19:41 +0000 Subject: Execute `ConfirmationValidator` validation when `_confirmation`'s value is `false` --- activemodel/lib/active_model/validations/confirmation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activemodel/lib') 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)) -- cgit v1.2.3 From c3675f50d2e59b7fc173d7b332860c4b1a24a726 Mon Sep 17 00:00:00 2001 From: Lisa Ugray Date: Thu, 19 Oct 2017 12:45:07 -0400 Subject: 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. --- activemodel/lib/active_model.rb | 2 + activemodel/lib/active_model/attribute.rb | 241 +++++++++++++++++++++ .../attribute/user_provided_default.rb | 30 +++ .../lib/active_model/attribute_mutation_tracker.rb | 114 ++++++++++ activemodel/lib/active_model/attribute_set.rb | 113 ++++++++++ .../lib/active_model/attribute_set/builder.rb | 124 +++++++++++ .../lib/active_model/attribute_set/yaml_encoder.rb | 41 ++++ activemodel/lib/active_model/attributes.rb | 56 +++-- activemodel/lib/active_model/dirty.rb | 177 ++++++++++----- activemodel/lib/active_model/type.rb | 4 + 10 files changed, 828 insertions(+), 74 deletions(-) create mode 100644 activemodel/lib/active_model/attribute.rb create mode 100644 activemodel/lib/active_model/attribute/user_provided_default.rb create mode 100644 activemodel/lib/active_model/attribute_mutation_tracker.rb create mode 100644 activemodel/lib/active_model/attribute_set.rb create mode 100644 activemodel/lib/active_model/attribute_set/builder.rb create mode 100644 activemodel/lib/active_model/attribute_set/yaml_encoder.rb (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index a97d7989be..faecd6b96f 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -30,6 +30,8 @@ require "active_model/version" module ActiveModel extend ActiveSupport::Autoload + autoload :Attribute + autoload :Attributes autoload :AttributeAssignment autoload :AttributeMethods autoload :BlockValidator, "active_model/validator" diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb new file mode 100644 index 0000000000..43130c37c5 --- /dev/null +++ b/activemodel/lib/active_model/attribute.rb @@ -0,0 +1,241 @@ +# frozen_string_literal: true + +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 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..9072460124 --- /dev/null +++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +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 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 + + def force_change(*) + 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..a892accbc6 --- /dev/null +++ b/activemodel/lib/active_model/attribute_set.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require "active_model/attribute_set/builder" +require "active_model/attribute_set/yaml_encoder" + +module ActiveModel + class AttributeSet # :nodoc: + delegate :each_value, :fetch, 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..f94f47370f --- /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, :always_initialized, :default + + def initialize(types, always_initialized = nil, &default) + @types = types + @always_initialized = always_initialized + @default = default + end + + def build_from_database(values = {}, additional_types = {}) + if always_initialized && !values.key?(always_initialized) + values[always_initialized] = nil + end + + attributes = LazyAttributeHash.new(types, values, additional_types, &default) + AttributeSet.new(attributes) + end + end + end + + class LazyAttributeHash # :nodoc: + delegate :transform_values, :each_key, :each_value, :fetch, to: :materialize + + def initialize(types, values, additional_types, &default) + @types = types + @values = values + @additional_types = additional_types + @materialized = false + @delegate_hash = {} + @default = default || proc {} + 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 + + 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) + delegate_hash[name] = default.call(name) || Attribute.uninitialized(name, type) + 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 index 3e34d3b83a..13cad87875 100644 --- a/activemodel/lib/active_model/attributes.rb +++ b/activemodel/lib/active_model/attributes.rb @@ -1,24 +1,30 @@ # frozen_string_literal: true +require "active_support/core_ext/object/deep_dup" require "active_model/type" +require "active_model/attribute_set" +require "active_model/attribute/user_provided_default" module ActiveModel module Attributes #:nodoc: extend ActiveSupport::Concern include ActiveModel::AttributeMethods - include ActiveModel::Dirty included do attribute_method_suffix "=" class_attribute :attribute_types, :_default_attributes, instance_accessor: false - self.attribute_types = {} - self._default_attributes = {} + self.attribute_types = Hash.new(Type.default_value) + self._default_attributes = AttributeSet.new({}) end module ClassMethods - def attribute(name, cast_type = Type::Value.new, **options) - self.attribute_types = attribute_types.merge(name.to_s => cast_type) - self._default_attributes = _default_attributes.merge(name.to_s => options[:default]) + 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_methods(name) end @@ -37,11 +43,29 @@ module ActiveModel 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 - clear_changes_information end private @@ -53,21 +77,17 @@ module ActiveModel attr_name.to_s end - cast_type = self.class.attribute_types[name] - - 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 + @attributes.write_from_user(attr_name.to_s, value) + value end - def attribute(name) - if instance_variable_defined?("@#{name}") - instance_variable_get("@#{name}") + def attribute(attr_name) + name = if self.class.attribute_alias?(attr_name) + self.class.attribute_alias(attr_name).to_s else - default = self.class._default_attributes[name] - default.respond_to?(:call) ? default.call : default + attr_name.to_s end + @attributes.fetch_value(name) end # Handle *= for method_missing. diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 943db0ab52..ddd93e34a6 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -2,6 +2,7 @@ require "active_support/hash_with_indifferent_access" require "active_support/core_ext/object/duplicable" +require "active_model/attribute_mutation_tracker" module ActiveModel # == Active \Model \Dirty @@ -130,6 +131,24 @@ 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: + @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. # # person.changed? # => false @@ -148,6 +167,60 @@ module ActiveModel changed_attributes.keys end + # Handles *_changed? 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 *_was for +method_missing+. + def attribute_was(attr) # :nodoc: + attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) + end + + # Handles *_previously_changed? 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 } + 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 attr => original value. + # + # 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 attr => [original value, new value]. # @@ -155,7 +228,9 @@ module ActiveModel # person.name = 'bob' # person.changes # => { "name" => ["bill", "bob"] } def changes - ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] + 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. @@ -166,45 +241,51 @@ module ActiveModel # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new + @previously_changed.merge(mutations_before_last_save.changes) end - # Returns a hash of the attributes with unsaved changes indicating their original - # values like attr => original value. - # - # person.name # => "bob" - # person.name = 'robert' - # person.changed_attributes # => {"name" => "bob"} - def changed_attributes - @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new + def attribute_changed_in_place?(attr_name) # :nodoc: + mutations_from_database.changed_in_place?(attr_name) end - # Handles *_changed? 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 + private + def clear_attribute_change(attr_name) + mutations_from_database.forget_change(attr_name) + end - # Handles *_was for +method_missing+. - def attribute_was(attr) # :nodoc: - attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) - 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 - # Handles *_previously_changed? for +method_missing+. - def attribute_previously_changed?(attr) #:nodoc: - previous_changes_include?(attr) - end + def forget_attribute_assignments + @attributes = @attributes.map(&:forgetting_assignment) if @attributes + end - # Restore all previous data of the provided attributes. - def restore_attributes(attributes = changed) - attributes.each { |attr| restore_attribute! attr } - end + def mutations_before_last_save + @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance + end - private + 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) + attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name) end alias attribute_changed_by_setter? changes_include? @@ -214,18 +295,6 @@ module ActiveModel previous_changes.include?(attr_name) 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 *_change for +method_missing+. def attribute_change(attr) [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr) @@ -238,15 +307,16 @@ module ActiveModel # Handles *_will_change! for +method_missing+. def attribute_will_change!(attr) - return if attribute_changed?(attr) + unless attribute_changed?(attr) + begin + value = _read_attribute(attr) + value = value.duplicable? ? value.clone : value + rescue TypeError, NoMethodError + end - begin - value = _read_attribute(attr) - value = value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError + set_attribute_was(attr, value) end - - set_attribute_was(attr, value) + mutations_from_database.force_change(attr) end # Handles restore_*! for +method_missing+. @@ -257,18 +327,13 @@ module ActiveModel 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: + 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 - - # Remove changes information for the provided attributes. - def clear_attribute_changes(attributes) # :doc: - attributes_changed_by_setter.except!(*attributes) - end end end diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb index b0ed67f28e..39324999c9 100644 --- a/activemodel/lib/active_model/type.rb +++ b/activemodel/lib/active_model/type.rb @@ -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) -- cgit v1.2.3 From 7d862ffb6265fe889635e2f8311261a4a37702f1 Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" Date: Fri, 10 Nov 2017 14:02:28 +0900 Subject: Fix "warning: instance variable @attributes not initialized" --- activemodel/lib/active_model/dirty.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index ddd93e34a6..d2ebd18107 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -257,7 +257,7 @@ module ActiveModel unless defined?(@mutations_from_database) @mutations_from_database = nil end - @mutations_from_database ||= if @attributes + @mutations_from_database ||= if defined?(@attributes) ActiveModel::AttributeMutationTracker.new(@attributes) else NullMutationTracker.instance @@ -265,7 +265,7 @@ module ActiveModel end def forget_attribute_assignments - @attributes = @attributes.map(&:forgetting_assignment) if @attributes + @attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes) end def mutations_before_last_save -- cgit v1.2.3 From c59ab795eee288d0a8d6842d2115d14f58f7badd Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" Date: Fri, 10 Nov 2017 14:25:16 +0900 Subject: Add missing requires Currently, executing the test with only `attribute_set_test.rb` results in an error. ``` ./bin/test -w test/cases/attribute_set_test.rb Run options: --seed 33470 # Running: E Error: ActiveModel::AttributeSetTest#test_#map_returns_a_new_attribute_set_with_the_changes_applied: NameError: uninitialized constant ActiveModel::AttributeSetTest::AttributeSet Did you mean? ActiveModel::Attributes ActiveModel::Attribute activemodel/test/cases/attribute_set_test.rb:235:in `block in ' bin/test test/cases/attribute_set_test.rb:234 ``` Added a missing require to fix this. Also, I suspect that this is the cause of failures in CI. Ref: https://travis-ci.org/rails/rails/jobs/299994708 --- activemodel/lib/active_model/attribute.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb index 43130c37c5..b75ff80b31 100644 --- a/activemodel/lib/active_model/attribute.rb +++ b/activemodel/lib/active_model/attribute.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/object/duplicable" + module ActiveModel class Attribute # :nodoc: class << self -- cgit v1.2.3 From 5ed618e192e9788094bd92c51255dda1c4fd0eae Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" Date: Fri, 10 Nov 2017 14:04:51 +0900 Subject: Fix "warning: assigned but unused variable - name" --- activemodel/lib/active_model/attributes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb index 13cad87875..6f84748804 100644 --- a/activemodel/lib/active_model/attributes.rb +++ b/activemodel/lib/active_model/attributes.rb @@ -77,7 +77,7 @@ module ActiveModel attr_name.to_s end - @attributes.write_from_user(attr_name.to_s, value) + @attributes.write_from_user(name, value) value end -- cgit v1.2.3 From 1cddc91a02be8627dd266d79af1ee2675218fdfe Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" Date: Fri, 10 Nov 2017 18:52:13 +0900 Subject: Add missing requires Currently, executing the test with only `attribute_test.rb` results in an error. ``` ./bin/test -w test/cases/attribute_test.rb Run options: --seed 41205 # Running: ....E Error: ActiveModel::AttributeTest#test_attributes_do_not_equal_attributes_with_different_types: NameError: uninitialized constant ActiveModel::AttributeTest::Type rails/activemodel/test/cases/attribute_test.rb:159:in `block in ' bin/test test/cases/attribute_test.rb:158 ``` Added a missing require to fix this. --- activemodel/lib/active_model/attribute.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb index b75ff80b31..0b669db58a 100644 --- a/activemodel/lib/active_model/attribute.rb +++ b/activemodel/lib/active_model/attribute.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/core_ext/object/duplicable" +require "active_model/type" module ActiveModel class Attribute # :nodoc: -- cgit v1.2.3 From fc7a6c738122ba9dd6eff4ede08d67e86a3f0ea7 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono Date: Fri, 10 Nov 2017 23:48:53 +0900 Subject: Add missing require "active_support/core_ext/hash/indifferent_access" https://travis-ci.org/rails/rails/jobs/300163454#L2236 --- activemodel/lib/active_model/attribute_mutation_tracker.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb index 9072460124..c67e1b809a 100644 --- a/activemodel/lib/active_model/attribute_mutation_tracker.rb +++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/hash/indifferent_access" + module ActiveModel class AttributeMutationTracker # :nodoc: OPTION_NOT_GIVEN = Object.new -- cgit v1.2.3 From 24b59434e6aca9679b9f86a41cfbb1a33e3d5619 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono Date: Sat, 11 Nov 2017 06:43:54 +0900 Subject: Add missing autoload `Type` (#31123) Attribute modules (`Attribute`, `Attributes`, `AttributeSet`) uses `Type`, but referencing `Type` before the modules still fail. ``` % ./bin/test -w test/cases/attribute_test.rb -n test_with_value_from_user_validates_the_value Run options: -n test_with_value_from_user_validates_the_value --seed 31876 E Error: ActiveModel::AttributeTest#test_with_value_from_user_validates_the_value: NameError: uninitialized constant ActiveModel::AttributeTest::Type /Users/kamipo/src/github.com/rails/rails/activemodel/test/cases/attribute_test.rb:233:in `block in ' bin/test test/cases/attribute_test.rb:232 Finished in 0.002985s, 335.0479 runs/s, 335.0479 assertions/s. 1 runs, 1 assertions, 0 failures, 1 errors, 0 skips ``` Probably we need more autoloading at least `Type`. --- activemodel/lib/active_model.rb | 1 + activemodel/lib/active_model/attribute.rb | 1 - activemodel/lib/active_model/attributes.rb | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index faecd6b96f..a3884206a6 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -47,6 +47,7 @@ module ActiveModel autoload :SecurePassword autoload :Serialization autoload :Translation + autoload :Type autoload :Validations autoload :Validator diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb index 0b669db58a..b75ff80b31 100644 --- a/activemodel/lib/active_model/attribute.rb +++ b/activemodel/lib/active_model/attribute.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "active_support/core_ext/object/duplicable" -require "active_model/type" module ActiveModel class Attribute # :nodoc: diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb index 6f84748804..cac461b549 100644 --- a/activemodel/lib/active_model/attributes.rb +++ b/activemodel/lib/active_model/attributes.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "active_support/core_ext/object/deep_dup" -require "active_model/type" require "active_model/attribute_set" require "active_model/attribute/user_provided_default" -- cgit v1.2.3 From 15cb4efadb61a8813967d3c25f4adfc9a918a0c0 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Fri, 10 Nov 2017 09:38:05 +0100 Subject: fix bug on added? method fix rubocop issues --- activemodel/lib/active_model/errors.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'activemodel/lib') 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. -- cgit v1.2.3 From cceeeb6e57f1cf8b24d507e2da9ed85d374c8bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 27 Nov 2017 13:01:15 -0500 Subject: Preparing for 5.2.0.beta1 release --- activemodel/lib/active_model/gem_version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index 39269c159c..29db52702b 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 = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end -- cgit v1.2.3 From 95b86e57a6c07bc636f8feab4afd4c7fe0ca512c Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Mon, 27 Nov 2017 14:06:51 -0700 Subject: Change how `AttributeSet::Builder` receives its defaults There are two concerns which are both being combined into one here, but both have the same goal. There are certain attributes which we want to always consider initialized. Previously, they were handled separately. The primary key (which is assumed to be backed by a database column) needs to be initialized, because there is a ton of code in Active Record that assumes `foo.id` will never raise. Additionally, we want attributes which aren't backed by a database column to always be initialized, since we would never receive a database value for them. Ultimately these two concerns can be combined into one. The old implementation hid a lot of inherent complexity, and is hard to optimize from the outside. We can simplify things significantly by just passing in a hash. This has slightly different semantics from the old behavior, in that `Foo.select(:bar).first.id` will return the default value for the primary key, rather than `nil` unconditionally -- however, the default value is always `nil` in practice. --- activemodel/lib/active_model/attribute_set.rb | 2 +- .../lib/active_model/attribute_set/builder.rb | 28 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model/attribute_set.rb b/activemodel/lib/active_model/attribute_set.rb index a892accbc6..54a5dd4064 100644 --- a/activemodel/lib/active_model/attribute_set.rb +++ b/activemodel/lib/active_model/attribute_set.rb @@ -5,7 +5,7 @@ require "active_model/attribute_set/yaml_encoder" module ActiveModel class AttributeSet # :nodoc: - delegate :each_value, :fetch, to: :attributes + delegate :each_value, :fetch, :except, to: :attributes def initialize(attributes) @attributes = attributes diff --git a/activemodel/lib/active_model/attribute_set/builder.rb b/activemodel/lib/active_model/attribute_set/builder.rb index f94f47370f..758eb830fc 100644 --- a/activemodel/lib/active_model/attribute_set/builder.rb +++ b/activemodel/lib/active_model/attribute_set/builder.rb @@ -5,35 +5,30 @@ require "active_model/attribute" module ActiveModel class AttributeSet # :nodoc: class Builder # :nodoc: - attr_reader :types, :always_initialized, :default + attr_reader :types, :default_attributes - def initialize(types, always_initialized = nil, &default) + def initialize(types, default_attributes = {}) @types = types - @always_initialized = always_initialized - @default = default + @default_attributes = default_attributes end def build_from_database(values = {}, additional_types = {}) - if always_initialized && !values.key?(always_initialized) - values[always_initialized] = nil - end - - attributes = LazyAttributeHash.new(types, values, additional_types, &default) + 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, to: :materialize + delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize - def initialize(types, values, additional_types, &default) + def initialize(types, values, additional_types, default_attributes) @types = types @values = values @additional_types = additional_types @materialized = false @delegate_hash = {} - @default = default || proc {} + @default_attributes = default_attributes end def key?(key) @@ -94,7 +89,7 @@ module ActiveModel protected - attr_reader :types, :values, :additional_types, :delegate_hash, :default + attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes def materialize unless @materialized @@ -117,7 +112,12 @@ module ActiveModel if value_present delegate_hash[name] = Attribute.from_database(name, value, type) elsif types.key?(name) - delegate_hash[name] = default.call(name) || Attribute.uninitialized(name, type) + attr = default_attributes[name] + if attr + delegate_hash[name] = attr.dup + else + delegate_hash[name] = Attribute.uninitialized(name, type) + end end end end -- cgit v1.2.3 From 2837d0f3347e747a8c12bd3c097bc7282072d42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 28 Nov 2017 00:01:45 -0500 Subject: Preparing for 5.2.0.beta2 release --- activemodel/lib/active_model/gem_version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index 29db52702b..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 = "beta1" + PRE = "beta2" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end -- cgit v1.2.3