From a89f8a922d8f37245f3ca60748adc5f4c8ee88d2 Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Sun, 22 Jun 2014 15:38:55 -0600 Subject: Move behavior of `read_attribute` to `AttributeSet` Moved `Builder` to its own file, as it started looking very weird once I added private methods to the `AttributeSet` class and the `Builder` class started to grow. Would like to refactor `fetch_value` to change to ```ruby self[name].value(&block) ``` But that requires the attributes to know about their name, which they currently do not. --- activerecord/lib/active_record.rb | 2 +- activerecord/lib/active_record/attribute.rb | 27 +++++++++++ .../lib/active_record/attribute_methods/read.rb | 13 ++--- activerecord/lib/active_record/attribute_set.rb | 41 +++++++++------- .../lib/active_record/attribute_set/builder.rb | 33 +++++++++++++ activerecord/test/cases/attribute_set_test.rb | 56 ++++++++++++++++++++++ 6 files changed, 144 insertions(+), 28 deletions(-) create mode 100644 activerecord/lib/active_record/attribute_set/builder.rb diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index ab85414277..17b00bbaea 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -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 diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index da8eb10dc6..6c0b81b3fe 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -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 diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 8c1cc128f7..10869dfc1e 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -81,17 +81,10 @@ module ActiveRecord # Returns the value of the attribute identified by attr_name 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 diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index ed2500a675..228e82f332 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -1,16 +1,32 @@ +require 'active_record/attribute_set/builder' + module ActiveRecord class AttributeSet # :nodoc: - delegate :[], :[]=, :fetch, :include?, :keys, :each_with_object, to: :attributes + delegate :[], :[]=, :each_with_object, to: :attributes + delegate :keys, to: :initialized_attributes def initialize(attributes) @attributes = attributes 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 @@ -30,23 +46,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 diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb new file mode 100644 index 0000000000..d91720d5e1 --- /dev/null +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -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 diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb index 402a611efa..35254b1087 100644 --- a/activerecord/test/cases/attribute_set_test.rb +++ b/activerecord/test/cases/attribute_set_test.rb @@ -61,5 +61,61 @@ module ActiveRecord assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash) assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h) 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 -- cgit v1.2.3