From bc3b6ea461ee82a4c34877168fd498b81f12763c Mon Sep 17 00:00:00 2001
From: Kevin Deisz <kevin.deisz@gmail.com>
Date: Tue, 29 May 2018 15:57:56 -0400
Subject: Reflection for attachments

Add the ability to reflect on the attachments that have been defined using ActiveRecord::Reflection.
---
 activerecord/lib/active_record/reflection.rb       | 77 +++++++++++++++++-----
 activestorage/CHANGELOG.md                         |  5 ++
 .../lib/active_storage/attached/macros.rb          | 12 ++++
 activestorage/test/models/reflection_test.rb       | 29 ++++++++
 4 files changed, 106 insertions(+), 17 deletions(-)
 create mode 100644 activestorage/test/models/reflection_test.rb

diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 22d195c9a4..231c785a30 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -11,27 +11,33 @@ module ActiveRecord
     included do
       class_attribute :_reflections, instance_writer: false, default: {}
       class_attribute :aggregate_reflections, instance_writer: false, default: {}
+      class_attribute :attachment_reflections, instance_writer: false, default: {}
     end
 
     def self.create(macro, name, scope, options, ar)
-      klass = \
-        case macro
-        when :composed_of
-          AggregateReflection
-        when :has_many
-          HasManyReflection
-        when :has_one
-          HasOneReflection
-        when :belongs_to
-          BelongsToReflection
-        else
-          raise "Unsupported Macro: #{macro}"
-        end
-
-      reflection = klass.new(name, scope, options, ar)
+      reflection = reflection_class_for(macro).new(name, scope, options, ar)
       options[:through] ? ThroughReflection.new(reflection) : reflection
     end
 
+    def self.reflection_class_for(macro)
+      case macro
+      when :composed_of
+        AggregateReflection
+      when :has_many
+        HasManyReflection
+      when :has_one
+        HasOneReflection
+      when :belongs_to
+        BelongsToReflection
+      when :has_one_attached
+        HasOneAttachedReflection
+      when :has_many_attached
+        HasManyAttachedReflection
+      else
+        raise "Unsupported Macro: #{macro}"
+      end
+    end
+
     def self.add_reflection(ar, name, reflection)
       ar.clear_reflections_cache
       name = name.to_s
@@ -42,14 +48,18 @@ module ActiveRecord
       ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
     end
 
+    def self.add_attachment_reflection(ar, name, reflection)
+      ar.attachment_reflections.merge!(name.to_s => reflection)
+    end
+
     # \Reflection enables the ability to examine the associations and aggregations of
     # Active Record classes and objects. This information, for example,
     # can be used in a form builder that takes an Active Record object
     # and creates input fields for all of the attributes depending on their type
     # and displays the associations to other objects.
     #
-    # MacroReflection class has info for AggregateReflection and AssociationReflection
-    # classes.
+    # MacroReflection class has info for the AggregateReflection and
+    # AssociationReflection classes.
     module ClassMethods
       # Returns an array of AggregateReflection objects for all the aggregations in the class.
       def reflect_on_all_aggregations
@@ -64,6 +74,21 @@ module ActiveRecord
         aggregate_reflections[aggregation.to_s]
       end
 
+      # Returns an array of reflection objects for all the attachments in the
+      # class.
+      def reflect_on_all_attachments
+        attachment_reflections.values
+      end
+
+      # Returns the reflection object for the named +attachment+.
+      #
+      #    User.reflect_on_attachment(:avatar)
+      #    # => the avatar reflection
+      #
+      def reflect_on_attachment(attachment)
+        attachment_reflections[attachment.to_s]
+      end
+
       # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value.
       #
       #   Account.reflections # => {"balance" => AggregateReflection}
@@ -136,6 +161,8 @@ module ActiveRecord
     #         HasOneReflection
     #         BelongsToReflection
     #         HasAndBelongsToManyReflection
+    #       HasOneAttachedReflection
+    #       HasManyAttachedReflection
     #     ThroughReflection
     #     PolymorphicReflection
     #     RuntimeReflection
@@ -412,6 +439,22 @@ module ActiveRecord
       end
     end
 
+    # Holds all the metadata about a has_one_attached attachment as it was
+    # specified in the Active Record class.
+    class HasOneAttachedReflection < MacroReflection #:nodoc:
+      def macro
+        :has_one_attached
+      end
+    end
+
+    # Holds all the metadata about a has_many_attached attachment as it was
+    # specified in the Active Record class.
+    class HasManyAttachedReflection < MacroReflection #:nodoc:
+      def macro
+        :has_many_attached
+      end
+    end
+
     # Holds all the metadata about an association as it was specified in the
     # Active Record class.
     class AssociationReflection < MacroReflection #:nodoc:
diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md
index c8911fe611..4aa551781b 100644
--- a/activestorage/CHANGELOG.md
+++ b/activestorage/CHANGELOG.md
@@ -1,3 +1,8 @@
+*   Add the ability to reflect on defined attachments using the existing
+    ActiveRecord reflection mechanism.
+
+    *Kevin Deisz*
+
 *   Variant arguments of `false` or `nil` will no longer be passed to the
     processor. For example, the following will not have the monochrome
     variation applied:
diff --git a/activestorage/lib/active_storage/attached/macros.rb b/activestorage/lib/active_storage/attached/macros.rb
index f99cf35680..6ad9fc43d7 100644
--- a/activestorage/lib/active_storage/attached/macros.rb
+++ b/activestorage/lib/active_storage/attached/macros.rb
@@ -48,6 +48,12 @@ module ActiveStorage
       else
         before_destroy { public_send(name).detach }
       end
+
+      ActiveRecord::Reflection.add_attachment_reflection(
+        self,
+        name,
+        ActiveRecord::Reflection.create(:has_one_attached, name, nil, { dependent: dependent }, self)
+      )
     end
 
     # Specifies the relation between multiple attachments and the model.
@@ -105,6 +111,12 @@ module ActiveStorage
       else
         before_destroy { public_send(name).detach }
       end
+
+      ActiveRecord::Reflection.add_attachment_reflection(
+        self,
+        name,
+        ActiveRecord::Reflection.create(:has_many_attached, name, nil, { dependent: dependent }, self)
+      )
     end
   end
 end
diff --git a/activestorage/test/models/reflection_test.rb b/activestorage/test/models/reflection_test.rb
new file mode 100644
index 0000000000..1ddfafc0f0
--- /dev/null
+++ b/activestorage/test/models/reflection_test.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActiveStorage::ReflectionTest < ActiveSupport::TestCase
+  test "allows reflecting for all attachment" do
+    expected_classes =
+      User.reflect_on_all_attachments.all? do |reflection|
+        reflection.is_a?(ActiveRecord::Reflection::HasOneAttachedReflection) ||
+          reflection.is_a?(ActiveRecord::Reflection::HasManyAttachedReflection)
+      end
+
+    assert expected_classes
+  end
+
+  test "allows reflecting on a singular has_one_attached attachment" do
+    reflection = User.reflect_on_attachment(:avatar)
+
+    assert_equal :avatar, reflection.name
+    assert_equal :has_one_attached, reflection.macro
+  end
+
+  test "allows reflecting on a singular has_many_attached attachment" do
+    reflection = User.reflect_on_attachment(:highlights)
+
+    assert_equal :highlights, reflection.name
+    assert_equal :has_many_attached, reflection.macro
+  end
+end
-- 
cgit v1.2.3


From ce337d1757fdd01e5f496f741e33275a7440b9ac Mon Sep 17 00:00:00 2001
From: Kevin Deisz <kevin.deisz@gmail.com>
Date: Thu, 31 May 2018 09:33:14 -0400
Subject: Move ActiveStorage reflection logic entirely into ActiveStorage

---
 activerecord/lib/active_record/reflection.rb   | 46 +------------------
 activestorage/lib/active_storage/engine.rb     |  9 ++++
 activestorage/lib/active_storage/reflection.rb | 63 ++++++++++++++++++++++++++
 activestorage/test/models/reflection_test.rb   |  4 +-
 4 files changed, 76 insertions(+), 46 deletions(-)
 create mode 100644 activestorage/lib/active_storage/reflection.rb

diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 231c785a30..c47e0dc03c 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -11,7 +11,6 @@ module ActiveRecord
     included do
       class_attribute :_reflections, instance_writer: false, default: {}
       class_attribute :aggregate_reflections, instance_writer: false, default: {}
-      class_attribute :attachment_reflections, instance_writer: false, default: {}
     end
 
     def self.create(macro, name, scope, options, ar)
@@ -29,10 +28,6 @@ module ActiveRecord
         HasOneReflection
       when :belongs_to
         BelongsToReflection
-      when :has_one_attached
-        HasOneAttachedReflection
-      when :has_many_attached
-        HasManyAttachedReflection
       else
         raise "Unsupported Macro: #{macro}"
       end
@@ -48,18 +43,14 @@ module ActiveRecord
       ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
     end
 
-    def self.add_attachment_reflection(ar, name, reflection)
-      ar.attachment_reflections.merge!(name.to_s => reflection)
-    end
-
     # \Reflection enables the ability to examine the associations and aggregations of
     # Active Record classes and objects. This information, for example,
     # can be used in a form builder that takes an Active Record object
     # and creates input fields for all of the attributes depending on their type
     # and displays the associations to other objects.
     #
-    # MacroReflection class has info for the AggregateReflection and
-    # AssociationReflection classes.
+    # MacroReflection class has info for AggregateReflection and AssociationReflection
+    # classes.
     module ClassMethods
       # Returns an array of AggregateReflection objects for all the aggregations in the class.
       def reflect_on_all_aggregations
@@ -74,21 +65,6 @@ module ActiveRecord
         aggregate_reflections[aggregation.to_s]
       end
 
-      # Returns an array of reflection objects for all the attachments in the
-      # class.
-      def reflect_on_all_attachments
-        attachment_reflections.values
-      end
-
-      # Returns the reflection object for the named +attachment+.
-      #
-      #    User.reflect_on_attachment(:avatar)
-      #    # => the avatar reflection
-      #
-      def reflect_on_attachment(attachment)
-        attachment_reflections[attachment.to_s]
-      end
-
       # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value.
       #
       #   Account.reflections # => {"balance" => AggregateReflection}
@@ -161,8 +137,6 @@ module ActiveRecord
     #         HasOneReflection
     #         BelongsToReflection
     #         HasAndBelongsToManyReflection
-    #       HasOneAttachedReflection
-    #       HasManyAttachedReflection
     #     ThroughReflection
     #     PolymorphicReflection
     #     RuntimeReflection
@@ -439,22 +413,6 @@ module ActiveRecord
       end
     end
 
-    # Holds all the metadata about a has_one_attached attachment as it was
-    # specified in the Active Record class.
-    class HasOneAttachedReflection < MacroReflection #:nodoc:
-      def macro
-        :has_one_attached
-      end
-    end
-
-    # Holds all the metadata about a has_many_attached attachment as it was
-    # specified in the Active Record class.
-    class HasManyAttachedReflection < MacroReflection #:nodoc:
-      def macro
-        :has_many_attached
-      end
-    end
-
     # Holds all the metadata about an association as it was specified in the
     # Active Record class.
     class AssociationReflection < MacroReflection #:nodoc:
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
index 99588cdd4b..519b9ae283 100644
--- a/activestorage/lib/active_storage/engine.rb
+++ b/activestorage/lib/active_storage/engine.rb
@@ -10,6 +10,8 @@ require "active_storage/previewer/video_previewer"
 require "active_storage/analyzer/image_analyzer"
 require "active_storage/analyzer/video_analyzer"
 
+require "active_storage/reflection"
+
 module ActiveStorage
   class Engine < Rails::Engine # :nodoc:
     isolate_namespace ActiveStorage
@@ -95,5 +97,12 @@ module ActiveStorage
         end
       end
     end
+
+    initializer "active_storage.reflection" do
+      ActiveSupport.on_load(:active_record) do
+        include Reflection::ActiveRecordExtensions
+        ActiveRecord::Reflection.singleton_class.prepend(Reflection::ReflectionExtension)
+      end
+    end
   end
 end
diff --git a/activestorage/lib/active_storage/reflection.rb b/activestorage/lib/active_storage/reflection.rb
new file mode 100644
index 0000000000..9074b20126
--- /dev/null
+++ b/activestorage/lib/active_storage/reflection.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+  module Reflection
+    # Holds all the metadata about a has_one_attached attachment as it was
+    # specified in the Active Record class.
+    class HasOneAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc:
+      def macro
+        :has_one_attached
+      end
+    end
+
+    # Holds all the metadata about a has_many_attached attachment as it was
+    # specified in the Active Record class.
+    class HasManyAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc:
+      def macro
+        :has_many_attached
+      end
+    end
+
+    module ReflectionExtension
+      def reflection_class_for(macro)
+        case macro
+        when :has_one_attached
+          HasOneAttachedReflection
+        when :has_many_attached
+          HasManyAttachedReflection
+        else
+          super
+        end
+      end
+
+      def add_attachment_reflection(ar, name, reflection)
+        ar.attachment_reflections.merge!(name.to_s => reflection)
+      end
+    end
+
+    module ActiveRecordExtensions
+      extend ActiveSupport::Concern
+
+      included do
+        class_attribute :attachment_reflections, instance_writer: false, default: {}
+      end
+
+      module ClassMethods
+        # Returns an array of reflection objects for all the attachments in the
+        # class.
+        def reflect_on_all_attachments
+          attachment_reflections.values
+        end
+
+        # Returns the reflection object for the named +attachment+.
+        #
+        #    User.reflect_on_attachment(:avatar)
+        #    # => the avatar reflection
+        #
+        def reflect_on_attachment(attachment)
+          attachment_reflections[attachment.to_s]
+        end
+      end
+    end
+  end
+end
diff --git a/activestorage/test/models/reflection_test.rb b/activestorage/test/models/reflection_test.rb
index 1ddfafc0f0..da866ca996 100644
--- a/activestorage/test/models/reflection_test.rb
+++ b/activestorage/test/models/reflection_test.rb
@@ -6,8 +6,8 @@ class ActiveStorage::ReflectionTest < ActiveSupport::TestCase
   test "allows reflecting for all attachment" do
     expected_classes =
       User.reflect_on_all_attachments.all? do |reflection|
-        reflection.is_a?(ActiveRecord::Reflection::HasOneAttachedReflection) ||
-          reflection.is_a?(ActiveRecord::Reflection::HasManyAttachedReflection)
+        reflection.is_a?(ActiveStorage::Reflection::HasOneAttachedReflection) ||
+          reflection.is_a?(ActiveStorage::Reflection::HasManyAttachedReflection)
       end
 
     assert expected_classes
-- 
cgit v1.2.3


From 6c7e6abfaad149da02dbec4e4f2bd62c5d68805f Mon Sep 17 00:00:00 2001
From: Kevin Deisz <kevin.deisz@gmail.com>
Date: Thu, 31 May 2018 21:15:51 -0400
Subject: Ensure reflection_class_for is private

---
 activerecord/lib/active_record/reflection.rb   | 51 ++++++++++++++------------
 activestorage/lib/active_storage/reflection.rb | 25 +++++++------
 2 files changed, 40 insertions(+), 36 deletions(-)

diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index c47e0dc03c..6d2f75a3ae 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -13,34 +13,37 @@ module ActiveRecord
       class_attribute :aggregate_reflections, instance_writer: false, default: {}
     end
 
-    def self.create(macro, name, scope, options, ar)
-      reflection = reflection_class_for(macro).new(name, scope, options, ar)
-      options[:through] ? ThroughReflection.new(reflection) : reflection
-    end
+    class << self
+      def create(macro, name, scope, options, ar)
+        reflection = reflection_class_for(macro).new(name, scope, options, ar)
+        options[:through] ? ThroughReflection.new(reflection) : reflection
+      end
 
-    def self.reflection_class_for(macro)
-      case macro
-      when :composed_of
-        AggregateReflection
-      when :has_many
-        HasManyReflection
-      when :has_one
-        HasOneReflection
-      when :belongs_to
-        BelongsToReflection
-      else
-        raise "Unsupported Macro: #{macro}"
+      def add_reflection(ar, name, reflection)
+        ar.clear_reflections_cache
+        name = name.to_s
+        ar._reflections = ar._reflections.except(name).merge!(name => reflection)
       end
-    end
 
-    def self.add_reflection(ar, name, reflection)
-      ar.clear_reflections_cache
-      name = name.to_s
-      ar._reflections = ar._reflections.except(name).merge!(name => reflection)
-    end
+      def add_aggregate_reflection(ar, name, reflection)
+        ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
+      end
 
-    def self.add_aggregate_reflection(ar, name, reflection)
-      ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
+      private
+        def reflection_class_for(macro)
+          case macro
+          when :composed_of
+            AggregateReflection
+          when :has_many
+            HasManyReflection
+          when :has_one
+            HasOneReflection
+          when :belongs_to
+            BelongsToReflection
+          else
+            raise "Unsupported Macro: #{macro}"
+          end
+        end
     end
 
     # \Reflection enables the ability to examine the associations and aggregations of
diff --git a/activestorage/lib/active_storage/reflection.rb b/activestorage/lib/active_storage/reflection.rb
index 9074b20126..04a1b20882 100644
--- a/activestorage/lib/active_storage/reflection.rb
+++ b/activestorage/lib/active_storage/reflection.rb
@@ -18,21 +18,22 @@ module ActiveStorage
       end
     end
 
-    module ReflectionExtension
-      def reflection_class_for(macro)
-        case macro
-        when :has_one_attached
-          HasOneAttachedReflection
-        when :has_many_attached
-          HasManyAttachedReflection
-        else
-          super
-        end
-      end
-
+    module ReflectionExtension # :nodoc:
       def add_attachment_reflection(ar, name, reflection)
         ar.attachment_reflections.merge!(name.to_s => reflection)
       end
+
+      private
+        def reflection_class_for(macro)
+          case macro
+          when :has_one_attached
+            HasOneAttachedReflection
+          when :has_many_attached
+            HasManyAttachedReflection
+          else
+            super
+          end
+        end
     end
 
     module ActiveRecordExtensions
-- 
cgit v1.2.3