aglushkov/serega

View on GitHub
lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# frozen_string_literal: true

class Serega
  module SeregaPlugins
    #
    # Plugin :activerecord_preloads
    # (depends on :preloads plugin, that must be loaded first)
    #
    # Automatically preloads associations to serialized objects
    #
    # It takes all defined preloads from serialized attributes (including attributes from serialized relations),
    # merges them into single associations hash and then uses ActiveRecord::Associations::Preloader
    # to preload all associations.
    #
    # @example
    #   class AppSerializer < Serega
    #     plugin :preloads,
    #       auto_preload_attributes_with_delegate: true,
    #       auto_preload_attributes_with_serializer: true,
    #       auto_hide_attributes_with_preload: true
    #
    #     plugin :activerecord_preloads
    #   end
    #
    #   class UserSerializer < AppSerializer
    #     # no preloads
    #     attribute :username
    #
    #     # preloads `:user_stats` as auto_preload_attributes_with_delegate option is true
    #     attribute :comments_count, delegate: { to: :user_stats }
    #
    #     # preloads `:albums` as auto_preload_attributes_with_serializer option is true
    #     attribute :albums, serializer: AlbumSerializer, hide: false
    #   end
    #
    #   class AlbumSerializer < AppSerializer
    #     # no preloads
    #     attribute :title
    #
    #     # preloads :downloads_count as manually specified
    #     attribute :downloads_count, preload: :downloads, value: proc { |album| album.downloads.count }
    #   end
    #
    #   UserSerializer.to_h(user) # => preloads {users_stats: {}, albums: { downloads: {} }}
    #
    module ActiverecordPreloads
      #
      # @return [Symbol] Plugin name
      #
      def self.plugin_name
        :activerecord_preloads
      end

      # Checks requirements to load plugin
      #
      # @param serializer_class [Class<Serega>] Current serializer class
      # @param opts [Hash] plugin options
      #
      # @return [void]
      #
      def self.before_load_plugin(serializer_class, **opts)
        opts.each_key do |key|
          raise SeregaError, "Plugin #{plugin_name.inspect} does not accept the #{key.inspect} option. No options are allowed"
        end

        unless serializer_class.plugin_used?(:preloads)
          raise SeregaError, "Plugin #{plugin_name.inspect} must be loaded after the :preloads plugin. Please load the :preloads plugin first"
        end

        if serializer_class.plugin_used?(:batch)
          raise SeregaError, "Plugin #{plugin_name.inspect} must be loaded before the :batch plugin"
        end
      end

      #
      # Applies plugin code to specific serializer
      #
      # @param serializer_class [Class<Serega>] Current serializer class
      # @param _opts [Hash] Plugin options
      #
      # @return [void]
      #
      def self.load_plugin(serializer_class, **_opts)
        require_relative "lib/preloader"

        serializer_class.include(InstanceMethods)
      end

      #
      # Overrides Serega class instance methods
      #
      module InstanceMethods
        private

        #
        # Override original #serialize method
        # Preloads associations to object before serialization
        #
        def serialize(object, _opts)
          object = add_preloads(object)
          super
        end

        def add_preloads(obj)
          return obj if obj.nil? || (obj.is_a?(Array) && obj.empty?)

          preloads = preloads() # `preloads()` method comes from :preloads plugin
          return obj if preloads.empty?

          Preloader.preload(obj, preloads)
        end
      end
    end

    register_plugin(ActiverecordPreloads.plugin_name, ActiverecordPreloads)
  end
end