Sunday, February 11, 2007

The ruby implementation of an example in Martin Fowler's Language Workbenches article

It take me 3 hours to figure out how to implement in ruby the example described in Language Workbenches. It is one of my effort to learn the ruby language. The implementation uses what Jamis Buck's in the article Writing Domain Specific Languages called Sandboxing Approach.
#file ./lib/data.rb
class ServiceCall
    attr_accessor :customer_name,:customer_ID,
          :call_type_code,:date_of_call_string
end
class Usage
    attr_accessor :customer_name,:customer_ID,
          :cycle,:read_date
end
#file ./lib/data_reader.rb
class Reader
    attr_accessor :result

    def initialize
        @result = []
        @strategies = {}
    end

    def process file_name
        File.open(file_name,'r') do |file|
            while line = file.gets
                process_line line
            end
        end
    end

    def process_line line
        return if is_blank(line) || is_comment(line)
        type_code = get_type_code(line)
        strategy = @strategies[type_code]
        msg_err = <<-END
            unable to find strategy for
            type_code #{type_code}
        END
        raise msg_err if nil==strategy
        @result << strategy.process(line)
    end

    def is_blank line
        line == ''
    end

    def is_comment line
        line[0..0] == '#'
    end

    def get_type_code line
        line[0..3]
    end

    def mapping type_code, type, &block
        @strategies[type_code] =
            ReaderStrategy.new type_code, type, &block
    end

end
#file ./lib/data_reader_strategy.rb
class ReaderStrategy
    attr_accessor :extractors
    
    def initialize type_code, type, &block
        @type_code, @type = type_code, type
        @extractors = []
        instance_eval &block if block_given?
    end

    def process line
        result = @type.new
        @extractors.each do |e|
            e.extract_field result, line
        end
        result
    end

    def extract from_to, field_name
        @extractors << FieldExtractor.new(from_to,
            field_name)
    end

end

class FieldExtractor
    attr_accessor :from_to, :field_name

    def initialize from_to, field_name
        @from_to, @field_name = from_to, field_name
    end

    def extract_field target, line
        target.instance_variable_set(inst_var_sym,
            line[@from_to])
    end

    def inst_var_sym
        result = '@' + @field_name
        result.to_sym
    end
end
#file ./test/sample.txt
#123456789012345678901234567890123456789012345678901234567890
SVCLFOWLER         10101MS0120050313.........................
SVCLHOHPE          10201DX0320050315........................
SVCLTWO           x10301MRP220050329..............................
USGE10301TWO          x50214..7050329...............................
#file ./test/test_data_reader.rb
require 'test/unit'
require File.join(File.dirname(__FILE__),
    '..', 'lib', 'data')
require File.join(File.dirname(__FILE__),
    '..', 'lib', 'data_reader')
require File.join(File.dirname(__FILE__),
    '..', 'lib', 'data_reader_strategy')
class TestReader < Test::Unit::TestCase
    def setup
        @reader = Reader.new()
    end

    def test_read_file
        @reader.mapping('SVCL', ServiceCall) do
            extract 4..18, 'customer_name'
            extract 19..23, 'customer_ID'
            extract 24..27, 'call_type_code'
            extract 28..35, 'date_of_call_string'
        end
        @reader.mapping('USGE', Usage) do
            extract 9..22, 'customer_name'
            extract 4..8, 'customer_ID'
            extract 30..30, 'cycle'
            extract 31..36, 'read_date'
        end

        file_name = File.join(File.dirname(__FILE__),
            'sample.txt')
        @reader.process(file_name)
        assert_equal 4, @reader.result.size
    end

    def test_is_blank
        assert @reader.is_blank('')
    end

    def test_is_comment
        assert @reader.is_comment('#1234')
    end

    def test_get_type_code
        @reader.mapping('SVCL', ServiceCall)
        assert_equal 'SVCL',
            @reader.get_type_code('SVCLFOWLER')
    end
end
#file ./test/test_data_reader_strategy.rb
require 'test/unit'
require File.join(File.dirname(__FILE__),
    '..', 'lib', 'data')
require File.join(File.dirname(__FILE__),
    '..', 'lib', 'data_reader_strategy')
class TestReaderStrategy < Test::Unit::TestCase
    def test_service_call
        strategy =  ReaderStrategy.new 'SVCL',
        ServiceCall do
            extract 4..18, 'customer_name'
        end
        assert_equal 1, strategy.extractors.size

        result = strategy.process('SVCLFOWLER')
        assert_equal ServiceCall, result.class
        assert_equal 'FOWLER', result.customer_name
    end

    def test_usage_call
        strategy =  ReaderStrategy.new 'USGE',
        Usage do
            extract 9..22, 'customer_name'
        end
        assert_equal 1, strategy.extractors.size

        result = strategy.process('USGE10301TWO')
        assert_equal Usage, result.class
        assert_equal 'TWO', result.customer_name
    end

end

No comments: