One of the most powerful concept of Ruby is metaprogramming. This make the language uniq, powerful and expressive allowing you to create DSL which is tailored to your need. I would try to guide you guys to create a basic DSL using ruby metaprogramming.

Step 1: Ruby blocks

Ruby are simple. You can write a method that can yield. Something like,

def report
  puts "Header"
  yield
  puts "Footer"
end

report do
  puts "From block"
end

Output

Header
From block
Footer

Step 2: Ruby blocks sending object

We can send objects from block method back to block caller.

def report
  puts "Header"
  yield 'From block'
  puts "Footer"
end

report do |var_from_block|
  puts var_from_block
end

Here is what is happening,

  • When report block is called from line #7, the flow control goes to line #2
  • In line #3 the control yields back to line #8
  • After the yield block is over, the control goes back to line #4, and continues execution

Output

Header
From block
Footer

Step 3: instance_eval

class Report
  def initialize(&block)
    puts "Header"
    instance_eval &block if block_given?
    puts "Footer"
  end
end

Report.new do
  puts "From block"
end

Here the puts in line #10 is run on the context of report object. The following example will make things clearer.

class Report
  def initialize(&block)
    puts "Header"
    instance_eval &block if block_given?
    puts "Footer"
  end

  def my_print(str)
    puts str
  end
end

Report.new do
  my_print "From block"
end

Here you see there is no my_print method defined globally but at line #14 the block has access to my_print method. It is because the block is evaluated with the context of a report object.

Step 4: First draft of DSL

class Report
  def initialize(data, &block)
    @data = data
    @columns = []
    instance_eval &block if block_given?
  end

  def column(column_name)
    @columns << column_name
  end

  def print
    @data.each do |row|
      @columns.each do |column|
        puts row[column]
      end
    end
  end
end

data = [
  {name: 'Jitu', status: 'Married'},
  {name: 'Razeen', status: 'Single'}
]
report = Report.new(data) do
  column :name
end

report.print()

Output

Jitu
Razeen

Step 5: Second draft of DSL

Lets add some more features to make more out of the DSL.

class Column
  attr_accessor :key, :title, :footer
  def initialize(key, options={})
    @key = key
    @title = options[:title] || key.capitalize
    @footer = options[:footer]
  end
end

class Report
  def initialize(data, &block)
    @data = data
    @columns = []
    instance_eval &block if block_given?
  end

  def column(key, options={})
    @columns << Column.new(key, options)
  end

  def print
    puts @columns.map { |column| column.title }.join(', ')
    puts '=' * 10
    @data.each do |row|
      puts @columns.map { |column| row[column.key] }.join(', ')
    end
    print_footer
  end

  def print_footer
    if @columns.any? { |column| column.footer }
      puts '=' * 10
      footers = @columns.map do |column|
        if column.footer
          values = @data.map do |row|
            row[column.key]
          end
          values.inject{ |sum, el| sum + el }.to_f / values.size
        else
          ' ' * 5
        end
      end
      puts footers.join(' ')
    end
  end
end
data = [
  {name: 'Jitu', status: 'Married', age: 33},
  {name: 'Razeen', status: 'Single', age: 2}
]
report = Report.new(data) do
  column :name, title: 'Nick name'
  column :age, footer: :avg
end

report.print()

Output

Nick name, Age
==========
Jitu, 33
Razeen, 2
==========
      17.5

Now you can go ahead and implement new features on this abstraction. For the source code used here you can have look here. I once build a gem using the same concept query_report. Here is a demo.