
module PiecePipe
  #
  # A specialization of Step, designed to operate on subsets of input and add information
  # to the flow.  Inputs arrive as Hashes, and outputs are generated by (effectively)
  # merging new key/value pairs on the Hash and producing the result.
  #
  # Instead of implementing #process, AssemblySteps should implement #receive.
  #
  # Instead of generating output with #produce, safely generate compound output
  # by calling #install with a hash of one or more interesting values.  As with
  # process/produce, you may call #install zero or more times, depending on your
  # need to filter, transform or expand the stream of objects.
  #
  # The input to #receive is a duplicate of the actual Hash object that was
  # produced by the previous step, so modifying the inputs directly will
  # neither help you, nor harm subsequent steps.
  #
  # AssemblySteps insist on consuming Hash-like objects; though the preceding step
  # in the pipeline needn't actually be an AssemblyStep, it must produce Hash-like 
  # objects as output.
  #
  # #install starts with a fresh copy of the inputs each time, so multiple 
  # invocations will not share the installed values.
  #
  # IF YOUR STEP DECIDES NOT TO INSTALL ANYTHING: be sure to call #noop
  # (equiv to calling #install({})) which will pass all inputs on to the next
  # step of the pipeline unmodified.  If you DON'T call #install, your 
  # step effectively becomes a filter, and the flow of objects will cease.
  #
  class AssemblyStep < Step
    def process(item)
      ensure_hash_like_object item
      @assembly = item
      begin
        receive(item)
      ensure
        @assembly = nil
      end
    end

    # Default implementation is a noop
    def receive(assembly)
      noop
    end

    def noop
      install({})
    end

    def install(value,opts={})
      output = @assembly.merge(value || {})
      
      if opts[:drop]
        opts[:drop].each do |key|
          output.delete(key)
        end
      end
      produce output
    end

    private
    def ensure_hash_like_object(obj)
      unless obj.respond_to?(:[]) and obj.respond_to?(:merge) and obj.respond_to?(:dup)
        raise "AssemblyStep object #{self.class.name} requires its source to produce Hash-like elements; not acceptable: #{obj.inspect}"
      end
    end
  end
end
