From bb7bc499e5f3355eee7955c4a2db7a9ee731bef4 Mon Sep 17 00:00:00 2001
From: Sean Griffin <sean@thoughtbot.com>
Date: Sun, 22 Jun 2014 16:57:40 -0600
Subject: `Attribute` should know about its name

This allows using polymorphism for the uninitialized attributes raising
an exception behavior.
---
 activerecord/lib/active_record/attribute.rb        | 47 +++++++++++++---------
 activerecord/lib/active_record/attribute_set.rb    | 14 +++----
 .../lib/active_record/attribute_set/builder.rb     |  7 ++--
 activerecord/test/cases/attribute_set_test.rb      |  3 ++
 activerecord/test/cases/attribute_test.rb          | 37 +++++++++++------
 5 files changed, 64 insertions(+), 44 deletions(-)

(limited to 'activerecord')

diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
index 13c8bc3676..33c20bb5cc 100644
--- a/activerecord/lib/active_record/attribute.rb
+++ b/activerecord/lib/active_record/attribute.rb
@@ -1,24 +1,29 @@
 module ActiveRecord
   class Attribute # :nodoc:
     class << self
-      def from_database(value, type)
-        FromDatabase.new(value, type)
+      def from_database(name, value, type)
+        FromDatabase.new(name, value, type)
       end
 
-      def from_user(value, type)
-        FromUser.new(value, type)
+      def from_user(name, value, type)
+        FromUser.new(name, value, type)
       end
 
-      def uninitialized(type)
-        Uninitialized.new(type)
+      def null(name)
+        Null.new(name)
+      end
+
+      def uninitialized(name, type)
+        Uninitialized.new(name, type)
       end
     end
 
-    attr_reader :value_before_type_cast, :type
+    attr_reader :name, :value_before_type_cast, :type
 
     # This method should not be called directly.
     # Use #from_database or #from_user
-    def initialize(value_before_type_cast, type)
+    def initialize(name, value_before_type_cast, type)
+      @name = name
       @value_before_type_cast = value_before_type_cast
       @type = type
     end
@@ -42,11 +47,11 @@ module ActiveRecord
     end
 
     def with_value_from_user(value)
-      self.class.from_user(value, type)
+      self.class.from_user(name, value, type)
     end
 
     def with_value_from_database(value)
-      self.class.from_database(value, type)
+      self.class.from_database(name, value, type)
     end
 
     def type_cast
@@ -77,9 +82,9 @@ module ActiveRecord
       end
     end
 
-    class NullAttribute < Attribute # :nodoc:
-      def initialize
-        super(nil, Type::Value.new)
+    class Null < Attribute # :nodoc:
+      def initialize(name)
+        super(name, nil, Type::Value.new)
       end
 
       def value
@@ -88,21 +93,23 @@ module ActiveRecord
     end
 
     class Uninitialized < Attribute # :nodoc:
-      def initialize(type)
-        super(nil, type)
+      def initialize(name, type)
+        super(name, nil, type)
       end
 
       def value
-        nil
+        if block_given?
+          yield name
+        end
+      end
+
+      def value_for_database
       end
-      alias value_for_database value
 
       def initialized?
         false
       end
     end
-    private_constant :FromDatabase, :FromUser, :NullAttribute, :Uninitialized
-
-    Null = NullAttribute.new # :nodoc:
+    private_constant :FromDatabase, :FromUser, :Null, :Uninitialized
   end
 end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
index 65e15b16dd..5be11e6ab9 100644
--- a/activerecord/lib/active_record/attribute_set.rb
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -2,13 +2,16 @@ require 'active_record/attribute_set/builder'
 
 module ActiveRecord
   class AttributeSet # :nodoc:
-    delegate :[], to: :attributes
     delegate :keys, to: :initialized_attributes
 
     def initialize(attributes)
       @attributes = attributes
     end
 
+    def [](name)
+      attributes[name] || Attribute.null(name)
+    end
+
     def values_before_type_cast
       attributes.each_with_object({}) { |(k, v), h| h[k] = v.value_before_type_cast }
     end
@@ -22,13 +25,8 @@ module ActiveRecord
       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
+    def fetch_value(name, &block)
+      self[name].value(&block)
     end
 
     def write_from_database(name, value)
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
index f9cf9c1809..1e146a07da 100644
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -16,16 +16,15 @@ module ActiveRecord
       private
 
       def build_attributes_from_values(values, additional_types)
-        attributes = Hash.new(Attribute::Null)
-        values.each_with_object(attributes) do |(name, value), hash|
+        values.each_with_object({}) do |(name, value), hash|
           type = additional_types.fetch(name, types[name])
-          hash[name] = Attribute.from_database(value, type)
+          hash[name] = Attribute.from_database(name, value, type)
         end
       end
 
       def add_uninitialized_attributes(attributes)
         types.except(*attributes.keys).each do |name, type|
-          attributes[name] = Attribute.uninitialized(type)
+          attributes[name] = Attribute.uninitialized(name, type)
         end
       end
     end
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
index 35caa0ddab..cdbb11fa32 100644
--- a/activerecord/test/cases/attribute_set_test.rb
+++ b/activerecord/test/cases/attribute_set_test.rb
@@ -8,6 +8,8 @@ module ActiveRecord
 
       assert_equal 1, attributes[:foo].value
       assert_equal 2.2, attributes[:bar].value
+      assert_equal :foo, attributes[:foo].name
+      assert_equal :bar, attributes[:bar].name
     end
 
     test "building with custom types" do
@@ -24,6 +26,7 @@ module ActiveRecord
 
       assert_equal '3.3', attributes[:foo].value_before_type_cast
       assert_equal nil, attributes[:bar].value_before_type_cast
+      assert_equal :bar, attributes[:bar].name
     end
 
     test "duping creates a new hash and dups each attribute" do
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
index 0adf9545b8..24452fdec2 100644
--- a/activerecord/test/cases/attribute_test.rb
+++ b/activerecord/test/cases/attribute_test.rb
@@ -13,7 +13,7 @@ module ActiveRecord
 
     test "from_database + read type casts from database" do
       @type.expect(:type_cast_from_database, 'type cast from database', ['a value'])
-      attribute = Attribute.from_database('a value', @type)
+      attribute = Attribute.from_database(nil, 'a value', @type)
 
       type_cast_value = attribute.value
 
@@ -22,7 +22,7 @@ module ActiveRecord
 
     test "from_user + read type casts from user" do
       @type.expect(:type_cast_from_user, 'type cast from user', ['a value'])
-      attribute = Attribute.from_user('a value', @type)
+      attribute = Attribute.from_user(nil, 'a value', @type)
 
       type_cast_value = attribute.value
 
@@ -31,7 +31,7 @@ module ActiveRecord
 
     test "reading memoizes the value" do
       @type.expect(:type_cast_from_database, 'from the database', ['whatever'])
-      attribute = Attribute.from_database('whatever', @type)
+      attribute = Attribute.from_database(nil, 'whatever', @type)
 
       type_cast_value = attribute.value
       second_read = attribute.value
@@ -42,14 +42,14 @@ module ActiveRecord
 
     test "reading memoizes falsy values" do
       @type.expect(:type_cast_from_database, false, ['whatever'])
-      attribute = Attribute.from_database('whatever', @type)
+      attribute = Attribute.from_database(nil, 'whatever', @type)
 
       attribute.value
       attribute.value
     end
 
     test "read_before_typecast returns the given value" do
-      attribute = Attribute.from_database('raw value', @type)
+      attribute = Attribute.from_database(nil, 'raw value', @type)
 
       raw_value = attribute.value_before_type_cast
 
@@ -59,7 +59,7 @@ module ActiveRecord
     test "from_database + read_for_database type casts to and from database" do
       @type.expect(:type_cast_from_database, 'read from database', ['whatever'])
       @type.expect(:type_cast_for_database, 'ready for database', ['read from database'])
-      attribute = Attribute.from_database('whatever', @type)
+      attribute = Attribute.from_database(nil, 'whatever', @type)
 
       type_cast_for_database = attribute.value_for_database
 
@@ -69,7 +69,7 @@ module ActiveRecord
     test "from_user + read_for_database type casts from the user to the database" do
       @type.expect(:type_cast_from_user, 'read from user', ['whatever'])
       @type.expect(:type_cast_for_database, 'ready for database', ['read from user'])
-      attribute = Attribute.from_user('whatever', @type)
+      attribute = Attribute.from_user(nil, 'whatever', @type)
 
       type_cast_for_database = attribute.value_for_database
 
@@ -78,7 +78,7 @@ module ActiveRecord
 
     test "duping dups the value" do
       @type.expect(:type_cast_from_database, 'type cast', ['a value'])
-      attribute = Attribute.from_database('a value', @type)
+      attribute = Attribute.from_database(nil, 'a value', @type)
 
       value_from_orig = attribute.value
       value_from_clone = attribute.dup.value
@@ -90,13 +90,13 @@ module ActiveRecord
 
     test "duping does not dup the value if it is not dupable" do
       @type.expect(:type_cast_from_database, false, ['a value'])
-      attribute = Attribute.from_database('a value', @type)
+      attribute = Attribute.from_database(nil, 'a value', @type)
 
       assert_same attribute.value, attribute.dup.value
     end
 
     test "duping does not eagerly type cast if we have not yet type cast" do
-      attribute = Attribute.from_database('a value', @type)
+      attribute = Attribute.from_database(nil, 'a value', @type)
       attribute.dup
     end
 
@@ -111,7 +111,7 @@ module ActiveRecord
     end
 
     test "with_value_from_user returns a new attribute with the value from the user" do
-      old = Attribute.from_database("old", MyType.new)
+      old = Attribute.from_database(nil, "old", MyType.new)
       new = old.with_value_from_user("new")
 
       assert_equal "old from database", old.value
@@ -119,11 +119,24 @@ module ActiveRecord
     end
 
     test "with_value_from_database returns a new attribute with the value from the database" do
-      old = Attribute.from_user("old", MyType.new)
+      old = Attribute.from_user(nil, "old", MyType.new)
       new = old.with_value_from_database("new")
 
       assert_equal "old from user", old.value
       assert_equal "new from database", new.value
     end
+
+    test "uninitialized attributes yield their name if a block is given to value" do
+      block = proc { |name| name.to_s + "!" }
+      foo = Attribute.uninitialized(:foo, nil)
+      bar = Attribute.uninitialized(:bar, nil)
+
+      assert_equal "foo!", foo.value(&block)
+      assert_equal "bar!", bar.value(&block)
+    end
+
+    test "uninitialized attributes have no value" do
+      assert_nil Attribute.uninitialized(:foo, nil).value
+    end
   end
 end
-- 
cgit v1.2.3