# frozen_string_literal: true class Serega module SeregaPlugins # # Plugin `:metadata` # # Depends on: `:root` plugin, that must be loaded first # # Adds ability to describe metadata that must be added to serialized response # # Added class-level method `:meta_attribute`, to define metadata, it accepts: # - *path [Array] - nested hash keys beginning from the root object. # - **options [Hash] - defaults are `hide_nil: false, hide_empty: false` # - &block [Proc] - describes value for current meta attribute # # @example # class AppSerializer < Serega # plugin :root # plugin :metadata # # meta_attribute(:version) { '1.2.3' } # meta_attribute(:ab_tests, :names) { %i[foo bar] } # meta_attribute(:meta, :paging, hide_nil: true) do |records, ctx| # next unless records.respond_to?(:total_count) # # { page: records.page, per_page: records.per_page, total_count: records.total_count } # end # end # # AppSerializer.to_h(nil) # => {:data=>nil, :version=>"1.2.3", :ab_tests=>{:names=>[:foo, :bar]}} # module Metadata # @return [Symbol] Plugin name def self.plugin_name :metadata end # Checks requirements and loads additional plugins # # @param serializer_class [Class] Current serializer class # @param _opts [Hash] loaded plugins opts # # @return [void] # def self.before_load_plugin(serializer_class, **_opts) unless serializer_class.plugin_used?(:root) raise SeregaError, "Please load :root plugin first so we can wrap serialization response into top-level hash to add metadata there" end end # # Applies plugin code to specific serializer # # @param serializer_class [Class] Current serializer class # @param _opts [Hash] Loaded plugins options # # @return [void] # def self.load_plugin(serializer_class, **_opts) serializer_class.extend(ClassMethods) serializer_class.include(InstanceMethods) serializer_class::SeregaConfig.include(ConfigInstanceMethods) require_relative "./meta_attribute" require_relative "./validations/check_block" require_relative "./validations/check_opt_hide_nil" require_relative "./validations/check_opt_hide_empty" require_relative "./validations/check_opts" require_relative "./validations/check_path" meta_attribute_class = Class.new(MetaAttribute) meta_attribute_class.serializer_class = serializer_class serializer_class.const_set(:MetaAttribute, meta_attribute_class) end # # Adds config options and runs other callbacks after plugin was loaded # # @param serializer_class [Class] Current serializer class # @param _opts [Hash] loaded plugins opts # # @return [void] # def self.after_load_plugin(serializer_class, **_opts) serializer_class.config.opts[:metadata] = {attribute_keys: %i[path hide_nil hide_empty]} end # # Config for `metadata` plugin # class MetadataConfig # @return [Hash] metadata options attr_reader :opts # # Initializes context_metadata config object # # @param opts [Hash] options # # @return [Serega::SeregaPlugins::Metadata::MetadataConfig] # def initialize(opts) @opts = opts end # # Returns allowed metadata attribute keys # def attribute_keys opts.fetch(:attribute_keys) end end # # Config class additional/patched instance methods # # @see Serega::SeregaConfig # module ConfigInstanceMethods # @return [Serega::SeregaPlugins::Metadata::MetadataConfig] metadata config def metadata @metadata ||= MetadataConfig.new(opts.fetch(:metadata)) end end # # Serega class additional/patched class methods # # @see Serega::SeregaConfig # module ClassMethods # # List of added metadata attributes # # @return [Hash Serega::SeregaPlugins::Metadata::MetaAttribute>] Added metadata attributes # def meta_attributes @meta_attributes ||= {} end # # Define metadata attribute # # @param path [String, Symbol] Metadata attribute path keys # @param opts [Hash] Metadata attribute options # @param block [Proc] Block to fetch metadata attribute value # # @return [Serega::SeregaPlugins::Metadata::MetaAttribute] Added metadata attribute # def meta_attribute(*path, **opts, &block) attribute = self::MetaAttribute.new(path: path, opts: opts, block: block) meta_attributes[attribute.name] = attribute end private def inherited(subclass) super meta_attribute_class = Class.new(self::MetaAttribute) meta_attribute_class.serializer_class = subclass subclass.const_set(:MetaAttribute, meta_attribute_class) # Assign same metadata attributes meta_attributes.each_value do |attr| subclass.meta_attribute(*attr.path, **attr.opts, &attr.block) end end end # # Serega additional/patched instance methods # # @see Serega # module InstanceMethods private def serialize(object, opts) result = super return result unless result.is_a?(Hash) # return earlier if not a hash, so no root was added root = build_root(object, opts) return result unless root # return earlier when no root add_metadata(object, opts[:context], result) result end def add_metadata(object, context, hash) self.class.meta_attributes.each_value do |meta_attribute| metadata = meta_attribute_value(object, context, meta_attribute) next unless metadata deep_merge_metadata(hash, metadata) end end def meta_attribute_value(object, context, meta_attribute) value = meta_attribute.value(object, context) return if meta_attribute.hide?(value) # Example: # [:foo, :bar].reverse_each.inject(:bazz) { |val, key| { key => val } } # => { foo: { bar: :bazz } } meta_attribute.path.reverse_each.inject(value) { |val, key| {key => val} } end def deep_merge_metadata(hash, metadata) hash.merge!(metadata) do |_key, this_val, other_val| if this_val.is_a?(Hash) && other_val.is_a?(Hash) deep_merge_metadata(this_val, other_val) else other_val end end end end end register_plugin(Metadata.plugin_name, Metadata) end end