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