lib/eco/language/models/parser_serializer.rb



module Eco
  module Language
    module Models
      # Basic class to define a parser/serializing framework

      # @attr_reader attr [String, Symbol] the attribute this parser/serializer is linked to.

      class ParserSerializer
        attr_reader :attr

        # Parser/serializer.

        # @param attr [String, Symbol] name of the parsed/serialized.

        # @param dependencies [Hash] provisioning of _**default dependencies**_

        #   that will be required when calling back to the

        #   parsing or serializing functions.

        def initialize(attr, dependencies: {})
          @attr         = attr
          @dependencies = dependencies
          @parser       = {}
          @serializer   = {}
        end

        # Defines the _parser_ of the attribute.

        # @note

        #   1. the _block_ should expect one or two parameters.

        #   2. the final dependencies is a merge of _default dependencies_ with `parse` call dependencies.

        # @param category [Symbol] a way to classify multiple parsers by category.

        # @yield [source_data, dependencies] user defined parser that returns the parsed value.

        # @yieldparam source_data [Any] source data that will be parsed.

        # @yieldparam dependencies [Hash] hash with the provisioned dependencies.

        def def_parser(category = :default, &block)
          @parser[category.to_sym] = block
          self
        end

        # Defines the _serializer_ of the attribute.

        # @note

        #   1. the block should expect one or two parameters.

        #   2. the final dependencies is a merge of _default dependencies_ with `serialize` call dependencies.

        # @param category [Symbol] a way to classify multiple serializers by category.

        # @yield [source_data, dependencies] user defined serialiser that returns the serialised value.

        # @yieldparam source_data [Any] source data that will be serialised.

        # @yieldparam dependencies [Hash] hash with the provisioned dependencies.

        def def_serializer(category = :default, &block)
          @serializer[category.to_sym] = block
          self
        end

        # Calls the `parser` of this attribute by passing `source` and resolved dependencies.

        # @note

        #   - the method depenencies override keys of the _default dependencies_.

        # @raise [Exception] when there is **no** `parser` defined.

        # @yield [key, value] the dependency resolver. The block is called is value is a Proc.

        # @yieldparam key [Symbol] the dependency key name.

        # @yieldparam value [Any] the depedency value.

        # @yieldreturn value the new value

        # @param source [Any] source data to be parsed.

        # @param dependencies [Hash] _additional dependencies_ that should be merged to the _default dependencies_.

        def parse(source, category = :default, dependencies: {}, &block)
          raise "There is no parser of type '#{category}' for this attribue '#{attr}'" unless parser_category?(category)

          deps = resolve_dependencies(@dependencies.merge(dependencies), &block)
          call_block(source, deps, attr, &@parser[category.to_sym])
        end

        # Calls the `serializer` of this attribute by passing `object` and resolved dependencies.

        # @note

        #   - the method depenencies override keys of the _default dependencies_.

        # @raise [Exception] when there is **no** `serializer` defined.

        # @param object [Any] source data to be serialized.

        # @param dependencies [Hash] _additional dependencies_ that should be merged to the _default dependencies_.

        def serialize(object, category = :default, dependencies: {}, &block)
          msg = "There is no serializer of type '#{category}' for this attribue '#{attr}'"
          raise msg unless serializer_category?(category)

          deps = resolve_dependencies(@dependencies.merge(dependencies), &block)
          call_block(object, deps, attr, &@serializer[category.to_sym])
        end

        # Checks if there's a `parser` defined for `category`

        # @return [Boolean] `true` if the parser is defined, and `false` otherwise

        def parser_category?(category = :default)
          @parser.key?(category.to_sym)
        end

        # Checks if there's a `serializer` defined for `category`

        # @return [Boolean] `true` if the serializer is defined, and `false` otherwise

        def serializer_category?(category = :default)
          @serializer.key?(category.to_sym)
        end

        private

        # For each Proc value it yields to resolve the dependency

        def resolve_dependencies(deps)
          return deps unless block_given?

          deps.dup.tap do |out|
            deps.each do |key, value|
              next unless value.is_a?(Proc)
              out[key] = yield(key, value)
            end
          end
        end

        # The methods may expect less parameters from some type of parsers.

        #   Here, we ensure they are called with the expected number of parameters.

        def call_block(*args, &block)
          params = block.parameters.zip(args).map(&:last)
          yield(*params)
        end
      end
    end
  end
end