Merge pull request #15868 from sgrif/sg-uninitialized-attributes

Move behavior of `read_attribute` to `AttributeSet`

Conflicts:
	activerecord/lib/active_record/attribute_set.rb
	activerecord/test/cases/attribute_set_test.rb
This commit is contained in:
Rafael Mendonça França 2014-06-26 06:40:08 -03:00
commit 2571c3f415
6 changed files with 144 additions and 28 deletions

View File

@ -27,12 +27,12 @@ require 'active_model'
require 'arel'
require 'active_record/version'
require 'active_record/attribute_set'
module ActiveRecord
extend ActiveSupport::Autoload
autoload :Attribute
autoload :AttributeSet
autoload :Base
autoload :Callbacks
autoload :Core

View File

@ -8,6 +8,10 @@ module ActiveRecord
def from_user(value, type)
FromUser.new(value, type)
end
def uninitialized(type)
Uninitialized.new(type)
end
end
attr_reader :value_before_type_cast, :type
@ -41,6 +45,10 @@ module ActiveRecord
raise NotImplementedError
end
def initialized?
true
end
protected
def initialize_dup(other)
@ -69,6 +77,25 @@ module ActiveRecord
false
end
alias changed_in_place_from? changed_from?
def initialized?
true
end
end
end
class Uninitialized < Attribute # :nodoc:
def initialize(type)
super(nil, type)
end
def value
nil
end
alias value_for_database value
def initialized?
false
end
end
end

View File

@ -81,17 +81,10 @@ module ActiveRecord
# Returns the value of the attribute identified by <tt>attr_name</tt> after
# it has been typecast (for example, "2004-12-12" in a date column is cast
# to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name)
def read_attribute(attr_name, &block)
name = attr_name.to_s
@attributes.fetch(name) {
if name == 'id'
return read_attribute(self.class.primary_key)
elsif block_given? && self.class.columns_hash.key?(name)
return yield(name)
else
return nil
end
}.value
name = self.class.primary_key if name == 'id'
@attributes.fetch_value(name, &block)
end
private

View File

@ -1,6 +1,9 @@
require 'active_record/attribute_set/builder'
module ActiveRecord
class AttributeSet # :nodoc:
delegate :[], :[]=, :fetch, :include?, :keys, to: :attributes
delegate :[], :[]=, to: :attributes
delegate :keys, to: :initialized_attributes
def initialize(attributes)
@attributes = attributes
@ -11,10 +14,23 @@ module ActiveRecord
end
def to_hash
attributes.each_with_object({}) { |(k, v), h| h[k] = v.value }
initialized_attributes.each_with_object({}) { |(k, v), h| h[k] = v.value }
end
alias_method :to_h, :to_hash
def include?(name)
attributes.include?(name) && self[name].initialized?
end
def fetch_value(name)
attribute = self[name]
if attribute.initialized? || !block_given?
attribute.value
else
yield name
end
end
def freeze
@attributes.freeze
super
@ -34,23 +50,14 @@ module ActiveRecord
super
end
class Builder # :nodoc:
def initialize(types)
@types = types
end
def build_from_database(values, additional_types = {})
attributes = Hash.new(Attribute::Null)
values.each_with_object(attributes) do |(name, value), hash|
type = additional_types.fetch(name, @types[name])
hash[name] = Attribute.from_database(value, type)
end
AttributeSet.new(attributes)
end
end
protected
attr_reader :attributes
private
def initialized_attributes
attributes.select { |_, attr| attr.initialized? }
end
end
end

View File

@ -0,0 +1,33 @@
module ActiveRecord
class AttributeSet # :nodoc:
class Builder # :nodoc:
attr_reader :types
def initialize(types)
@types = types
end
def build_from_database(values, additional_types = {})
attributes = build_attributes_from_values(values, additional_types)
add_uninitialized_attributes(attributes)
AttributeSet.new(attributes)
end
private
def build_attributes_from_values(values, additional_types)
attributes = Hash.new(Attribute::Null)
values.each_with_object(attributes) do |(name, value), hash|
type = additional_types.fetch(name, types[name])
hash[name] = Attribute.from_database(value, type)
end
end
def add_uninitialized_attributes(attributes)
types.except(*attributes.keys).each do |name, type|
attributes[name] = Attribute.uninitialized(type)
end
end
end
end
end

View File

@ -68,5 +68,61 @@ module ActiveRecord
assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast)
end
test "known columns are built with uninitialized attributes" do
attributes = attributes_with_uninitialized_key
assert attributes[:foo].initialized?
assert_not attributes[:bar].initialized?
end
test "uninitialized attributes are not included in the attributes hash" do
attributes = attributes_with_uninitialized_key
assert_equal({ foo: 1 }, attributes.to_hash)
end
test "uninitialized attributes are not included in keys" do
attributes = attributes_with_uninitialized_key
assert_equal [:foo], attributes.keys
end
test "uninitialized attributes return false for include?" do
attributes = attributes_with_uninitialized_key
assert attributes.include?(:foo)
assert_not attributes.include?(:bar)
end
test "unknown attributes return false for include?" do
attributes = attributes_with_uninitialized_key
assert_not attributes.include?(:wibble)
end
test "fetch_value returns the value for the given initialized attribute" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
assert_equal 1, attributes.fetch_value(:foo)
assert_equal 2.2, attributes.fetch_value(:bar)
end
test "fetch_value returns nil for unknown attributes" do
attributes = attributes_with_uninitialized_key
assert_nil attributes.fetch_value(:wibble)
end
test "fetch_value uses the given block for uninitialized attributes" do
attributes = attributes_with_uninitialized_key
value = attributes.fetch_value(:bar) { |n| n.to_s + '!' }
assert_equal 'bar!', value
end
test "fetch_value returns nil for uninitialized attributes if no block is given" do
attributes = attributes_with_uninitialized_key
assert_nil attributes.fetch_value(:bar)
end
def attributes_with_uninitialized_key
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
builder.build_from_database(foo: '1.1')
end
end
end