pragmarb/pragma-decorator

View on GitHub
lib/pragma/decorator/association/adapter/active_record.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module Pragma
  module Decorator
    module Association
      module Adapter
        # The ActiveRecord association adapter is used in AR environments and tries to minimize the
        # number of SQL queries that are made to retrieve the associated object's data.
        #
        # @api private
        class ActiveRecord < Base
          include Adaptor

          class << self
            # Returns whether the adapter supports the given bond.
            #
            # @param bond [Bond] the bond to check
            #
            # @return [Boolean] whether the object is an instance of +ActiveRecord::Base+
            def supports?(bond)
              Object.const_defined?('::ActiveRecord::Base') &&
                bond.model.is_a?(::ActiveRecord::Base)
            end
          end

          # Initializes the adapter.
          #
          # @param bond [Bond] the bond to use in the adapter
          #
          # @raise [InconsistentTypeError] when the association's real type is different from the
          #   one defined on the decorator ()e.g. decorator defines the association as +belongs_to+,
          #   but ActiveRecord reports its type is +has_one+)
          def initialize(bond)
            super
            check_type_consistency
          end

          # Returns the primary key of the associated object.
          #
          # If the +exec_context+ of the association is +decorator+, this will simply return early
          # with the value returned by +#id+ on the associated object.
          #
          # If the association is a +belongs_to+, there are three possible scenarios:
          #
          #   * the association does not have a custom scope: this will compute the PK by calling
          #     the foreign key on the parent model;
          #   * the association has a custom scope and it has not been loaded: this will compute
          #     the PK by +pluck+ing the PK column of the associated object;
          #   * the association has a custom scope and it has been loaded: this will compute
          #     the PK by retrieving the PK attribute from the loaded object.
          #
          # If the association is a +has_one+, there are two possible scenarios:
          #
          #   * the association has already been loaded: this will compute the PK by retrieving the
          #     PK attribute from the loaded object;
          #   * the association has not been loaded: this will compute the PK by +pluck+ing the PK
          #     column of the associated object;
          #
          # Custom scopes are always respected in both +belongs_to+ and +has_one+.
          #
          # +nil+ values are handled gracefully in all cases.
          #
          # @return [String|Integer|NilClass] the PK of the associated object
          #
          # @todo Allow to specify a different PK attribute when +exec_context+ is +decorator+
          def primary_key
            return associated_object&.id if association_reflection.nil? ||
                                            reflection.options[:exec_context] == :decorator

            case reflection.type
            when :belongs_to
              compute_belongs_to_fk
            when :has_one
              compute_has_one_fk
            else
              fail "Cannot compute primary key for #{reflection.type} association"
            end
          end

          # Returns the expanded associated object.
          #
          # This will simply return the associated object itself, delegating caching to AR.
          #
          # @return [Object] the associated object
          #
          # @todo Ensure the required attributes are present on the associated object
          def full_object
            associated_object
          end

          private

          def compute_belongs_to_fk
            primary_key = if association_reflection.polymorphic?
              association_reflection.options[:primary_key] || associated_object.class.primary_key
            else
              association_reflection.association_primary_key
            end

            if model.association(reflection.attribute).loaded?
              return associated_object&.public_send(primary_key)
            end

            if association_reflection.scope.nil?
              return model.public_send(association_reflection.foreign_key)
            end

            pluck_association_fk do |scope|
              fk = model.public_send(association_reflection.foreign_key)
              scope.where(primary_key => fk)
            end
          end

          def compute_has_one_fk
            if model.association(reflection.attribute).loaded?
              return associated_object&.public_send(association_reflection.association_primary_key)
            end

            pluck_association_fk do |scope|
              pk = model.public_send(association_reflection.active_record_primary_key)
              scope.where(association_reflection.foreign_key => pk)
            end
          end

          def pluck_association_fk
            scope = association_reflection.klass.all

            if association_reflection.scope
              scope = scope.instance_eval(&association_reflection.scope)
            end

            yield(scope).pluck(association_reflection.association_primary_key).first
          end

          def association_reflection
            @association_reflection ||= model.class.reflect_on_association(reflection.attribute)
          end

          def check_type_consistency
            return unless association_reflection
            return if association_reflection.macro.to_sym == reflection.type.to_sym

            fail InconsistentTypeError.new(
              decorator: decorator,
              reflection: reflection,
              model_type: association_reflection.macro
            )
          end
        end
      end
    end
  end
end