aglushkov/serega

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

Summary

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

class Serega
  module SeregaPlugins
    #
    # Plugin :context_metadata
    #
    # Depends on: `:root` plugin, that must be loaded first
    #
    # Allows to specify metadata to be added to serialized response.
    #
    # @example
    #   class UserSerializer < Serega
    #     plugin :root, root: :data
    #     plugin :context_metadata, context_metadata_key: :meta
    #   end
    #
    #   UserSerializer.to_h(nil, meta: { version: '1.0.1' })
    #   # => {:data=>nil, :version=>"1.0.1"}
    #
    module ContextMetadata
      # Default context metadata option name
      DEFAULT_CONTEXT_METADATA_KEY = :meta

      # @return [Symbol] Plugin name
      def self.plugin_name
        :context_metadata
      end

      # Checks requirements and loads additional plugins
      #
      # @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 = %i[context_metadata_key]
        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" \
            "  - :context_metadata_key [Symbol] - The key name that must be used to add metadata. Default is :meta."
        end

        unless serializer_class.plugin_used?(:root)
          raise SeregaError, "Plugin #{plugin_name.inspect} must be loaded after the :root plugin. Please load the :root plugin first"
        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)
        serializer_class.include(InstanceMethods)
        serializer_class::SeregaConfig.include(ConfigInstanceMethods)
        serializer_class::CheckSerializeParams.include(CheckSerializeParamsInstanceMethods)
      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
        meta_key = opts[:context_metadata_key] || DEFAULT_CONTEXT_METADATA_KEY
        config.opts[:context_metadata] = {key: meta_key}
        config.serialize_keys << meta_key
      end

      #
      # Config for `context_metadata` plugin
      #
      class ContextMetadataConfig
        # @return [Hash] context_metadata options
        attr_reader :opts

        #
        # Initializes context_metadata config object
        #
        # @param opts [Hash] options
        #
        # @return [Serega::SeregaPlugins::ContextMetadata::ContextMetadataConfig]
        def initialize(opts)
          @opts = opts
        end

        # Key that should be used to define metadata
        def key
          opts.fetch(:key)
        end

        # Sets key that should be used to define metadata
        #
        # @param new_key [Symbol] New key
        #
        # @return [Symbol] New key
        def key=(new_key)
          opts[:key] = new_key
        end
      end

      #
      # Config class additional/patched instance methods
      #
      # @see Serega::SeregaConfig
      #
      module ConfigInstanceMethods
        # @return [Serega::SeregaPlugins::ContextMetadata::ContextMetadataConfig] context_metadata config
        def context_metadata
          @context_metadata ||= ContextMetadataConfig.new(opts.fetch(:context_metadata))
        end
      end

      #
      # CheckSerializeParams class additional/patched instance methods
      #
      # @see Serega::SeregaValidations::CheckSerializeParams
      #
      module CheckSerializeParamsInstanceMethods
        private

        def check_opts
          super

          meta_key = self.class.serializer_class.config.context_metadata.key
          SeregaValidations::Utils::CheckOptIsHash.call(opts, meta_key)
        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_context_metadata(result, opts)
          result
        end

        def add_context_metadata(hash, opts)
          context_metadata_key = self.class.config.context_metadata.key
          return unless context_metadata_key

          metadata = opts[context_metadata_key]
          return unless metadata

          deep_merge_context_metadata(hash, metadata)
        end

        def deep_merge_context_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_context_metadata(this_val, other_val)
            else
              other_val
            end
          end
        end
      end
    end

    register_plugin(ContextMetadata.plugin_name, ContextMetadata)
  end
end