lib/serega.rb
# frozen_string_literal: true
require_relative "serega/version"
# Parent class for your serializers
class Serega
# Frozen hash
# @return [Hash] frozen hash
FROZEN_EMPTY_HASH = {}.freeze
# Frozen array
# @return [Array] frozen array
FROZEN_EMPTY_ARRAY = [].freeze
end
require_relative "serega/errors"
require_relative "serega/helpers/serializer_class_helper"
require_relative "serega/utils/enum_deep_dup"
require_relative "serega/utils/enum_deep_freeze"
require_relative "serega/utils/params_count"
require_relative "serega/utils/symbol_name"
require_relative "serega/utils/to_hash"
require_relative "serega/json/adapter"
require_relative "serega/attribute"
require_relative "serega/attribute_normalizer"
require_relative "serega/validations/utils/check_allowed_keys"
require_relative "serega/validations/utils/check_extra_keyword_arg"
require_relative "serega/validations/utils/check_opt_is_bool"
require_relative "serega/validations/utils/check_opt_is_hash"
require_relative "serega/validations/utils/check_opt_is_string_or_symbol"
require_relative "serega/validations/attribute/check_block"
require_relative "serega/validations/attribute/check_name"
require_relative "serega/validations/attribute/check_opt_const"
require_relative "serega/validations/attribute/check_opt_hide"
require_relative "serega/validations/attribute/check_opt_delegate"
require_relative "serega/validations/attribute/check_opt_many"
require_relative "serega/validations/attribute/check_opt_method"
require_relative "serega/validations/attribute/check_opt_serializer"
require_relative "serega/validations/attribute/check_opt_value"
require_relative "serega/validations/initiate/check_modifiers"
require_relative "serega/validations/check_attribute_params"
require_relative "serega/validations/check_initiate_params"
require_relative "serega/validations/check_serialize_params"
require_relative "serega/config"
require_relative "serega/object_serializer"
require_relative "serega/plan_point"
require_relative "serega/plan"
require_relative "serega/plugins"
class Serega
@config = SeregaConfig.new
# Validates `Serializer.attribute` params
check_attribute_params_class = Class.new(SeregaValidations::CheckAttributeParams)
check_attribute_params_class.serializer_class = self
const_set(:CheckAttributeParams, check_attribute_params_class)
# Validates `Serializer#new` params
check_initiate_params_class = Class.new(SeregaValidations::CheckInitiateParams)
check_initiate_params_class.serializer_class = self
const_set(:CheckInitiateParams, check_initiate_params_class)
# Validates `serializer#call(obj, PARAMS)` params
check_serialize_params_class = Class.new(SeregaValidations::CheckSerializeParams)
check_serialize_params_class.serializer_class = self
const_set(:CheckSerializeParams, check_serialize_params_class)
#
# Serializers class methods
#
module ClassMethods
# Returns current config
# @return [SeregaConfig] current serializer config
attr_reader :config
#
# Enables plugin for current serializer
#
# @param name [Symbol, Class<Module>] Plugin name or plugin module itself
# @param opts [Hash>] Plugin options
#
# @return [class<Module>] Loaded plugin module
#
def plugin(name, **opts)
raise SeregaError, "This plugin is already loaded" if plugin_used?(name)
plugin = SeregaPlugins.find_plugin(name)
# We split loading of plugin to three parts - before_load, load, after_load:
#
# - **before_load_plugin** usually used to check requirements and to load additional plugins
# - **load_plugin** usually used to include plugin modules
# - **after_load_plugin** usually used to add config options
plugin.before_load_plugin(self, **opts) if plugin.respond_to?(:before_load_plugin)
plugin.load_plugin(self, **opts) if plugin.respond_to?(:load_plugin)
plugin.after_load_plugin(self, **opts) if plugin.respond_to?(:after_load_plugin)
# Store attached plugins, so we can check it is loaded later
config.plugins << (plugin.respond_to?(:plugin_name) ? plugin.plugin_name : plugin)
plugin
end
#
# Checks plugin is used
#
# @param name [Symbol, Class<Module>] Plugin name or plugin module itself
#
# @return [Boolean] Is plugin used
#
def plugin_used?(name)
plugin_name =
case name
when Module then name.respond_to?(:plugin_name) ? name.plugin_name : name
else name
end
config.plugins.include?(plugin_name)
end
#
# Lists attributes
#
# @return [Hash] attributes list
#
def attributes
@attributes ||= {}
end
#
# Adds attribute
#
# Patched in:
# - plugin :presenter (additionally adds method in Presenter class)
#
# @param name [Symbol] Attribute name. Attribute value will be found by executing `object.<name>`
# @param opts [Hash] Options to serialize attribute
# @param block [Proc] Custom block to find attribute value. Accepts object and context.
#
# @return [Serega::SeregaAttribute] Added attribute
#
def attribute(name, **opts, &block)
attribute = self::SeregaAttribute.new(name: name, opts: opts, block: block)
attributes[attribute.name] = attribute
end
#
# Serializes provided object to Hash
#
# @param object [Object] Serialized object
# @param opts [Hash, nil] Serializer modifiers and other instantiating options
# @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
# @option opts [Array, Hash, String, Symbol] :except Attributes to hide
# @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
# @option opts [Boolean] :validate Validates provided modifiers (Default is true)
# @option opts [Hash] :context Serialization context
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
#
# @return [Hash] Serialization result
#
def call(object, opts = nil)
opts ||= FROZEN_EMPTY_HASH
initiate_keys = config.initiate_keys
if opts.empty?
modifiers_opts = FROZEN_EMPTY_HASH
serialize_opts = nil
else
serialize_opts = opts.except(*initiate_keys)
modifiers_opts = opts.slice(*initiate_keys)
end
new(modifiers_opts).to_h(object, serialize_opts)
end
#
# Serializes provided object to Hash
#
# @param object [Object] Serialized object
# @param opts [Hash, nil] Serializer modifiers and other instantiating options
# @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
# @option opts [Array, Hash, String, Symbol] :except Attributes to hide
# @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
# @option opts [Boolean] :validate Validates provided modifiers (Default is true)
# @option opts [Hash] :context Serialization context
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
#
# @return [Hash] Serialization result
#
def to_h(object, opts = nil)
call(object, opts)
end
#
# Serializes provided object to JSON string
#
# @param object [Object] Serialized object
# @param opts [Hash, nil] Serializer modifiers and other instantiating options
# @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
# @option opts [Array, Hash, String, Symbol] :except Attributes to hide
# @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
# @option opts [Boolean] :validate Validates provided modifiers (Default is true)
# @option opts [Hash] :context Serialization context
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
#
# @return [String] Serialization result
#
def to_json(object, opts = nil)
config.to_json.call(to_h(object, opts))
end
#
# Serializes provided object as JSON
#
# @param object [Object] Serialized object
# @param opts [Hash, nil] Serializer modifiers and other instantiating options
# @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
# @option opts [Array, Hash, String, Symbol] :except Attributes to hide
# @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
# @option opts [Boolean] :validate Validates provided modifiers (Default is true)
# @option opts [Hash] :context Serialization context
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
#
# @return [Hash] Serialization result
#
def as_json(object, opts = nil)
config.from_json.call(to_json(object, opts))
end
private
# Patched in:
# - plugin :batch (defines SeregaBatchLoaders, SeregaBatchLoader)
# - plugin :metadata (defines MetaAttribute and copies meta_attributes to subclasses)
# - plugin :presenter (defines Presenter)
def inherited(subclass)
config_class = Class.new(self::SeregaConfig)
config_class.serializer_class = subclass
subclass.const_set(:SeregaConfig, config_class)
subclass.instance_variable_set(:@config, subclass::SeregaConfig.new(config.opts))
attribute_class = Class.new(self::SeregaAttribute)
attribute_class.serializer_class = subclass
subclass.const_set(:SeregaAttribute, attribute_class)
attribute_normalizer_class = Class.new(self::SeregaAttributeNormalizer)
attribute_normalizer_class.serializer_class = subclass
subclass.const_set(:SeregaAttributeNormalizer, attribute_normalizer_class)
plan_class = Class.new(self::SeregaPlan)
plan_class.serializer_class = subclass
subclass.const_set(:SeregaPlan, plan_class)
plan_point_class = Class.new(self::SeregaPlanPoint)
plan_point_class.serializer_class = subclass
subclass.const_set(:SeregaPlanPoint, plan_point_class)
object_serializer_class = Class.new(self::SeregaObjectSerializer)
object_serializer_class.serializer_class = subclass
subclass.const_set(:SeregaObjectSerializer, object_serializer_class)
check_attribute_params_class = Class.new(self::CheckAttributeParams)
check_attribute_params_class.serializer_class = subclass
subclass.const_set(:CheckAttributeParams, check_attribute_params_class)
check_initiate_params_class = Class.new(self::CheckInitiateParams)
check_initiate_params_class.serializer_class = subclass
subclass.const_set(:CheckInitiateParams, check_initiate_params_class)
check_serialize_params_class = Class.new(self::CheckSerializeParams)
check_serialize_params_class.serializer_class = subclass
subclass.const_set(:CheckSerializeParams, check_serialize_params_class)
# Assign same attributes
attributes.each_value do |attr|
params = attr.initials
subclass.attribute(params[:name], **params[:opts], ¶ms[:block])
end
super
end
end
#
# Serializers instance methods
#
module InstanceMethods
#
# Instantiates new Serega class
#
# @param opts [Hash, nil] Serializer modifiers and other instantiating options
# @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
# @option opts [Array, Hash, String, Symbol] :except Attributes to hide
# @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
# @option opts [Boolean] :validate Validates provided modifiers (Default is true)
#
def initialize(opts = nil)
@opts = (opts.nil? || opts.empty?) ? FROZEN_EMPTY_HASH : parse_modifiers(opts)
self.class::CheckInitiateParams.new(@opts).validate if opts&.fetch(:check_initiate_params) { config.check_initiate_params }
@plan = self.class::SeregaPlan.call(@opts)
end
#
# Plan for serialization.
# This plan can be traversed to find serialized attributes and nested attributes.
#
# @return [Serega::SeregaPlan] Serialization plan
attr_reader :plan
#
# Serializes provided object to Hash
#
# @param object [Object] Serialized object
# @param opts [Hash, nil] Serializer modifiers and other instantiating options
# @option opts [Hash] :context Serialization context
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
#
# @return [Hash] Serialization result
#
def call(object, opts = nil)
self.class::CheckSerializeParams.new(opts).validate if opts&.any?
opts ||= {}
opts[:context] ||= {}
serialize(object, opts)
end
# @see #call
def to_h(object, opts = nil)
call(object, opts)
end
#
# Serializes provided object to JSON string
#
# @param object [Object] Serialized object
# @param opts [Hash, nil] Serializer modifiers and other instantiating options
# @option opts [Hash] :context Serialization context
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
#
# @return [Hash] Serialization result
#
def to_json(object, opts = nil)
hash = to_h(object, opts)
config.to_json.call(hash)
end
#
# Serializes provided object as JSON
#
# @param object [Object] Serialized object
# @param opts [Hash, nil] Serializer modifiers and other instantiating options
# @option opts [Hash] :context Serialization context
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
#
# @return [Hash] Serialization result
#
def as_json(object, opts = nil)
json = to_json(object, opts)
config.from_json.call(json)
end
private
attr_reader :opts
def config
self.class.config
end
def parse_modifiers(opts)
result = {}
opts.each do |key, value|
value = parse_modifier(value) if (key == :only) || (key == :except) || (key == :with)
result[key] = value
end
result
end
# Patched in:
# - plugin :string_modifiers (parses string modifiers differently)
def parse_modifier(value)
SeregaUtils::ToHash.call(value)
end
# Patched in:
# - plugin :activerecord_preloads (loads defined :preloads to object)
# - plugin :batch (runs serialization of collected batches)
# - plugin :root (wraps result `{ root => result }`)
# - plugin :context_metadata (adds context metadata to final result)
# - plugin :metadata (adds metadata to final result)
def serialize(object, opts)
self.class::SeregaObjectSerializer
.new(**opts, plan: plan)
.serialize(object)
end
end
extend ClassMethods
include InstanceMethods
end