Tuesday, February 20, 2007

Ruby's DSL implementation pattern

DSLs are designed to solve a particular kind of problem. Implementation DSLs as internal DSLs in Ruby is getting popular. Jim Freeze in his article Creating DSLs with Ruby use "Interpret" pattern to implement DSL. According the article a problem will be solved by using instance of a Class Machine. The Machine has class method load that create an instance of the Class Machine and load a file containing machine's instructions writen in DSL. The Machine also has instance method process, that run loaded instructions with specified data. A Ruby implementation can be as follow
class Machine
  def self.load(filename)
    machine = Machine.new
    machine.instance_eval(File.read(filename), 
      filename)
    machine
  end

  def process(*arg)
  #...
  end
end
Using this pattern, in The ruby implementation of an example in Martin Fowler's Language Workbenches article the Reader class can be modified as follows
class Reader
    #...

    def self.load dsl_file_name
        reader = Reader.new
        reader.instance_eval(File.read(dsl_file_name), 
          dsl_file_name)
        reader
    end

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

    #...

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

end
#dsl.txt
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
mapping('USGE', Usage) do
  extract 9..22, 'customer_name'
  extract 4..8, 'customer_ID'
  extract 30..30, 'cycle'
  extract 31..36, 'read_date'
end
#load_dsl.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 TestLoadDsl < Test::Unit::TestCase
    def test_read_file_load_dsl
        dsl_file_name =  File.join(File.dirname(__FILE__),
            'dsl.txt')
        reader = Reader.load dsl_file_name

        data_file_name = File.join(File.dirname(__FILE__),
            'sample.txt')
        reader.process data_file_name

        assert_equal 4, reader.result.size
    end
end

No comments: