lib/serega/plugins/preloads/preloads.rb
# frozen_string_literal: true
class Serega
module SeregaPlugins
#
# Plugin `:preloads`
#
# Allows to define `:preloads` to attributes and then allows to merge preloads
# from serialized attributes and return single associations hash.
#
# Plugin accepts options:
# - `auto_preload_attributes_with_delegate` - default false
# - `auto_preload_attributes_with_serializer` - default false
# - `auto_hide_attributes_with_preload` - default false
#
# This options are very handy if you want to forget about finding preloads manually.
#
# Preloads can be disabled with `preload: false` attribute option.
# Also automatically added preloads can be overwritten with manually specified `preload: :another_value`.
#
# Some examples, **please read comments in the code below**
#
# @example
# class AppSerializer < Serega
# plugin :preloads,
# auto_preload_attributes_with_delegate: true,
# auto_preload_attributes_with_serializer: true,
# auto_hide_attributes_with_preload: true
# end
#
# class UserSerializer < AppSerializer
# # No preloads
# attribute :username
#
# # Specify `preload: :user_stats` manually
# attribute :followers_count, preload: :user_stats, value: proc { |user| user.user_stats.followers_count }
#
# # Automatically `preloads: :user_stats` as `auto_preload_attributes_with_delegate` option is true
# attribute :comments_count, delegate: { to: :user_stats }
#
# # Automatically `preloads: :albums` as `auto_preload_attributes_with_serializer` option is true
# attribute :albums, serializer: 'AlbumSerializer'
# end
#
# class AlbumSerializer < AppSerializer
# attribute :images_count, delegate: { to: :album_stats }
# end
#
# # By default preloads are empty, as we specify `auto_hide_attributes_with_preload = true`,
# # and attributes with preloads will be not serialized
# UserSerializer.new.preloads # => {}
# UserSerializer.new.to_h(OpenStruct.new(username: 'foo')) # => {:username=>"foo"}
#
# UserSerializer.new(with: :followers_count).preloads # => {:user_stats=>{}}
# UserSerializer.new(with: %i[followers_count comments_count]).preloads # => {:user_stats=>{}}
# UserSerializer.new(with: [:followers_count, :comments_count, { albums: :images_count }]).preloads # => {:user_stats=>{}, :albums=>{:album_stats=>{}}}
#
module Preloads
DEFAULT_CONFIG = {
auto_preload_attributes_with_delegate: false,
auto_preload_attributes_with_serializer: false,
auto_hide_attributes_with_preload: false
}.freeze
private_constant :DEFAULT_CONFIG
# @return [Symbol] Plugin name
def self.plugin_name
: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)
allowed_keys = DEFAULT_CONFIG.keys
opts.each_key do |key|
next if allowed_keys.include?(key)
raise SeregaError,
"Plugin #{plugin_name.inspect} does not accept the #{key.inspect} option. Allowed options:\n" \
" - :auto_preload_attributes_with_delegate [Boolean] - Automatically adds `preload: <delegate_to>` option to attributes with :delegate option specified\n" \
" - :auto_preload_attributes_with_serializer [Boolean] - Automatically adds `preload: <attribute_name>` option to attributes with :serializer option specified\n" \
" - :auto_hide_attributes_with_preload [Boolean] - Automatically adds `hide: true` option to attributes with :preload option (specified manually or added automatically)"
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/format_user_preloads"
require_relative "lib/modules/attribute"
require_relative "lib/modules/attribute_normalizer"
require_relative "lib/modules/check_attribute_params"
require_relative "lib/modules/config"
require_relative "lib/modules/plan_point"
require_relative "lib/preload_paths"
require_relative "lib/preloads_config"
require_relative "lib/preloads_constructor"
require_relative "validations/check_opt_preload"
require_relative "validations/check_opt_preload_path"
serializer_class.include(InstanceMethods)
serializer_class::SeregaAttribute.include(AttributeInstanceMethods)
serializer_class::SeregaAttributeNormalizer.include(AttributeNormalizerInstanceMethods)
serializer_class::SeregaConfig.include(ConfigInstanceMethods)
serializer_class::SeregaPlanPoint.include(PlanPointInstanceMethods)
serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
end
#
# Adds config options and runs other callbacks after plugin was loaded
#
# @param serializer_class [Class<Serega>] Current serializer class
# @param opts [Hash] Plugin options
#
# @return [void]
#
def self.after_load_plugin(serializer_class, **opts)
config = serializer_class.config
config.attribute_keys << :preload << :preload_path
preloads_opts = DEFAULT_CONFIG.merge(opts.slice(*DEFAULT_CONFIG.keys))
config.opts[:preloads] = {}
preloads_config = config.preloads
preloads_config.auto_preload_attributes_with_delegate = preloads_opts[:auto_preload_attributes_with_delegate]
preloads_config.auto_preload_attributes_with_serializer = preloads_opts[:auto_preload_attributes_with_serializer]
preloads_config.auto_hide_attributes_with_preload = preloads_opts[:auto_hide_attributes_with_preload]
end
#
# Serega additional/patched instance methods
#
# @see Serega
#
module InstanceMethods
# @return [Hash] merged preloads of all serialized attributes
def preloads
@preloads ||= PreloadsConstructor.call(plan)
end
end
end
register_plugin(Preloads.plugin_name, Preloads)
end
end