# Martin Fowler's Reader Framework example from his "Language Workbenches"
# and "Generating Code for DSLs" papers.  
# H. Conrad Cunningham 
# Original:  4 October 2006 
# Revision: 10 October 2006

# READER FRAMEWORK

# This module implements a Reader Framework that enables reading data files
# with the following format:

#    #123456789012345678901234567890123456789012345678901234567890
#    SVCLFOWLER         10101MS0120050313.........................
#    SVCLHOHPE          10201DX0320050315........................
#    SVCLTWO           x10301MRP220050329..............................
#    USGE10301TWO          x50214..7050329...............................

# This framework must be configured with appropriate ReaderStrategy and
# FieldExtractor objects to handle a specific application of the family.

# Issues:
#   - Some of the error handling should be redesigned to use exceptions.
#   - Robustness should be improved.
#   - Testing should be improved.
#   - Documentation should be improved.
#   - This does not necessarily make the best usage of the features of the
#     Ruby language.
#   - May want to use the BlankSlate approach to remove method names from 
#     objects for the lines.

# Method Reader#process reads lines from its argument IO stream and creates
# an array of objects corresponding the lines in the file.  Comment lines,
# beginning with a "#", and blank lines are skipped.  
# 
# The first four characters on the other lines denote a type code for the
# data represented by the line.  The type code denotes the type and format
# of the data recorded on the line.  The class of the result objects also
# depend upon the type code.  The input data are transformed from the input
# line to the object using a strategy object for each type code.  

# The Reader#add_strategy method enables users of the class to provide the
# strategy objects for use in the transformation.

module ReaderFramework

  require 'ReaderUtilities'

  class Reader

    include ReaderUtilities

    CODE_BEGIN  = 0
    CODE_LENGTH = 4

    def initialize
      @strategies = {}
    end

    def process(input) 
      result = []
      input.each do |line|
        process_line(line,result)
      end
      result
    end

    def add_strategy(arg)
      @strategies[arg.code] = arg
    end

  private

    def process_line(line,result)
      trimmed = line.rstrip
      return result if is_blank? trimmed
      return result if is_comment? trimmed
      type_code = get_type_code(trimmed)
      strategy = @strategies[type_code]
      if strategy != nil 
        result << strategy.process(trimmed)
      else
        STDERR.puts "Unable to find strategy for #{type_code}"
      end
      result
    end

    def get_type_code(line)
      line[CODE_BEGIN,CODE_LENGTH]
    end

  end#Reader

  # Class ReaderStrategy encapsulates the strategies for translating an
  # input line beginning with the "code" string to an object of classname
  # "target".  It uses an array of FieldExtractor objects to do the
  # translation from the input column ranges in the input line to instance
  # variables in the target objects.

  # The ReaderStrategy#add_field_extractor method allows a client to add the
  # needed field extractor objects.

  # The ReaderStrategy#process method translates an input line to the
  # appropriate target object.

  class ReaderStrategy

    attr_reader :code

    def initialize(code,target)
      @code = code
      @target_class = target
      @extractors   = []
    end
  
    def add_field_extractor(begin_col, end_col, field_name)
      @extractors << FieldExtractor.new(begin_col, end_col, field_name)
    end

    def process(line)
      if @target_class.kind_of? Class
        result = @target_class.new
      elsif @target_class.respond_to? :to_s
        instance_eval("result = #{@target_class.to_s}.new")
      else
        STDERR.puts "Invalid target class name in ReaderStrategy."
      end  
      @extractors.each {|ex| ex.extract_field(line, result) }
      result
    end

  end#ReaderStrategy

  # Class FieldExtractor encapsulates the methods for extracting the fields
  # from the input and inserting them into the target object as values of
  # instance variables.

  # Method FieldExtractor#extract_field extracts a range of columns from a
  # line and injects the values extracted into appropriately named instance
  # variables of the target object.

  class FieldExtractor

    include ReaderUtilities

    def initialize(begin_col, end_col, field_name)
      @begin = begin_col
      @end   = end_col
      @field_name = clean_field_name(field_name)
    end

    def extract_field(line, target_object)
      value = line[@begin, @end - @begin + 1]
      target_object.instance_variable_set(@field_name.to_sym,value)
    end

  private

    def clean_field_name(name)
      if name[0,1] == '@'
        base = name[1..-1]
      else
        base = name
      end
      if base =~ /\W/
        STDERR.puts "Illegal character in field name \"#{name}\"."
        base = "ERROR_" + base.gsub(/\W/,'')
      end
      base = "@" + from_camel(base)
    end

  end#FieldExtractor

end#ReaderFramework
