module Krikri
  ##
  # Handles transformation of OriginalRecords into a target class.
  #
  # @example 
  #    map = Mapping.new(MyModelClass)
  #    map.dsl_method args
  #    map.process_record(my_original_record)
  #    # => #<MyModelClass:0x3ff8b7459210()>
  #
  # When one or more errors are encoutered during processing, they are collected
  # in a `Krikri::Kapping::Error` and re-raised.
  #
  # @example When an error is thrown during property mapping
  #    map = Mapping.new(MyModelClass)
  #    begin
  #      map.process_record(my_original_record)
  #    rescue Mapping::Error => e
  #      e.message
  #    end
  #    # => Property failed on subject:
  #    #       {subject error message}
  #    #       {subject error backtrace}
  #    #    Property failed on title:
  #    #       {title error message}
  #    #       {title error backtrace}
  #   
  # @see Krikri::MappingDSL
  class Mapping
    include MappingDSL

    attr_reader :klass, :parser, :parser_args

    ##
    # @param klass [Class] The model class to build in the mapping process.
    # @param parser [Class] The parser class with which to process resources.
    # @param parser_args [Array] The arguments to pass to the parser when
    #   processing records.
    def initialize(klass = DPLA::MAP::Aggregation,
                   parser = Krikri::XmlParser,
                   *parser_args)
      @klass = klass
      @parser = parser
      @parser_args = parser_args
    end

    ##
    # @param record [OriginalRecord] An original record to process.
    #
    # @return [Object] A model object of type @klass, processed through the
    #   mapping DSL
    # @raise [Krikri::Mapper::Error] when an error is thrown when handling any
    #   of the properties
    def process_record(record)
      mapped_record = klass.new
      error = properties.each_with_object(Error.new(record)) do |prop, error|
        begin
          prop.to_proc.call(mapped_record, parser.parse(record, *@parser_args))
        rescue => e
          error.add(prop.name, e)
        end
      end
      raise error unless error.errors.empty?
      mapped_record
    end
    
    ##
    # An error class for exceptions thrown during `Krikri::Mapping` processes.
    #
    # Collects the full set of errors encountered when mapping a given record,
    # along with the property names that were being processed when throwing the 
    # error.
    #
    # @example collecting exceptions and reraising
    #   err = Krikri::Mapping::Error.new(record)
    #   err.add(:title, exception)
    #   raise err
    #
    class Error < RuntimeError
      attr_accessor :original_record, :errors

      ##
      # @param [Krikri::OriginalRecord] record
      def initialize(record)
        @original_record = record
        @errors = {}
      end

      ##
      # @param [Symbol] property  the name of the property for the error
      # @param [Exception] parent_error  the error to add
      def add(property, parent_error)
        errors[property] = parent_error
      end

      ##
      # @return [Array<Symbol>] the property names that caused errors
      def properties
        errors.keys
      end
      
      ##
      # @return [String] a message describing the full error set
      def message
        msg = "Error processing mapping for #{original_record.local_name}\n"
        errors.each do |property, error|
          msg << "Failed on property #{property}:\n"
          msg << "\t#{error.message}\n\t#{error.backtrace.join("\n\t")}"
        end

        msg
      end
    end
  end
end