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) raise "There is no serializer of type '#{category}' for this attribue '#{attr}'" 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