diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb index 42d00b7248..613d188c37 100644 --- a/activemodel/lib/active_model/attribute.rb +++ b/activemodel/lib/active_model/attribute.rb @@ -5,16 +5,16 @@ require "active_support/core_ext/object/duplicable" module ActiveModel class Attribute # :nodoc: class << self - def from_database(name, value, type) - FromDatabase.new(name, value, type) + def from_database(name, value_before_type_cast, type, value = nil) + FromDatabase.new(name, value_before_type_cast, type, nil, value) end - def from_user(name, value, type, original_attribute = nil) - FromUser.new(name, value, type, original_attribute) + def from_user(name, value_before_type_cast, type, original_attribute = nil) + FromUser.new(name, value_before_type_cast, type, original_attribute) end - def with_cast_value(name, value, type) - WithCastValue.new(name, value, type) + def with_cast_value(name, value_before_type_cast, type) + WithCastValue.new(name, value_before_type_cast, type) end def null(name) @@ -30,11 +30,12 @@ module ActiveModel # This method should not be called directly. # Use #from_database or #from_user - def initialize(name, value_before_type_cast, type, original_attribute = nil) + def initialize(name, value_before_type_cast, type, original_attribute = nil, value = nil) @name = name @value_before_type_cast = value_before_type_cast @type = type @original_attribute = original_attribute + @value = value unless value.nil? end def value diff --git a/activemodel/lib/active_model/attribute_set/builder.rb b/activemodel/lib/active_model/attribute_set/builder.rb index eb92be692d..57df36d24a 100644 --- a/activemodel/lib/active_model/attribute_set/builder.rb +++ b/activemodel/lib/active_model/attribute_set/builder.rb @@ -14,11 +14,17 @@ module ActiveModel def build_from_database(values = {}, additional_types = {}) attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes) - AttributeSet.new(attributes) + LazyAttributeSet.new(attributes) end end end + class LazyAttributeSet < AttributeSet # :nodoc: + def fetch_value(name, &block) + attributes.fetch_value(name, &block) + end + end + class LazyAttributeHash # :nodoc: delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize @@ -26,9 +32,10 @@ module ActiveModel @types = types @values = values @additional_types = additional_types - @materialized = false - @delegate_hash = delegate_hash @default_attributes = default_attributes + @delegate_hash = delegate_hash + @casted_values = {} + @materialized = false end def key?(key) @@ -43,6 +50,25 @@ module ActiveModel delegate_hash[key] = value end + def fetch_value(name, &block) + if attr = delegate_hash[name] + return attr.value(&block) + end + + @casted_values.fetch(name) do + value_present = true + value = values.fetch(name) { value_present = false } + + if value_present + type = additional_types.fetch(name, types[name]) + @casted_values[name] = type.deserialize(value) + else + attr = assign_default_value(name, value_present, value) || Attribute.null(name) + attr.value(&block) + end + end + end + def deep_dup dup.tap do |copy| copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup)) @@ -104,13 +130,15 @@ module ActiveModel private attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes - def assign_default_value(name) - type = additional_types.fetch(name, types[name]) - value_present = true + def assign_default_value( + name, + value_present = true, value = values.fetch(name) { value_present = false } + ) + type = additional_types.fetch(name, types[name]) if value_present - delegate_hash[name] = Attribute.from_database(name, value, type) + delegate_hash[name] = Attribute.from_database(name, value, type, @casted_values[name]) elsif types.key?(name) attr = default_attributes[name] if attr