# 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