masaj salonu masaj salonları
Home » Advertising » Understanding Ruby Metaprogramming and DSLs

Understanding Ruby Metaprogramming and DSLs

A DSL, or Domain Specific Language, is a language that has a specific purpose rather than a general purpose, like C or Java. One of the most popular DSLs is SQL because it’s the standard for querying a relational database and its syntax is specific to activities such as sorting, filtering, and displaying data.

SQL falls under the category of external DSLs, which means it requires its own parser and implementation of all of the necessary components. Other examples of external DSLs are Gherkin, for writing feature files, Make Files, for building C and C++ applications, and HTML, for declaring webpages).

What Is a Ruby DSL?

Ruby is a powerful and expressive language that can be leveraged to create an internal DSL. Instead of requiring a full-syntax parser and an underlying implementation, metaprogramming techniques can be used to create a DSL that leverages the Ruby language.

Anyone that has used a popular Ruby framework has most likely seen a Ruby DSL. Frameworks that leverage these include:

  • Rake
  • RSpec
  • Active Record
  • Sinatra

Before RSpec came along, TestUnit was the most popular testing framework in Ruby. A test looked a lot like a typical Ruby class:

require "test/unit"

class TestCalculator  Test::Unit::TestCase
  def test_simple
    assert_equal(4, Calculator.add(2, 2) )
  end
end

RSpec changed the way programmers write test cases by creating an internal DSL that is much more expressive.

describe TestCalculator do
  it "should return the sum of the two numbers" do
   expect(Calculator.add(2, 2)).to eq(4)
  end
end

Creating a Simple Ruby DSL

In this tutorial, we’ll create a DSL for querying a CSV file.

The full file can be found here.

Step 1: Create a Standard Ruby Class

Look at the plain old Ruby code from this branch.

The class is here:

require 
"csv"

class WorldCupDSL
 attr_reader :conditions

 def initialize file_path
   @conditions = {}
   @data = CSV.read(file_path, headers: true, header_converters: :symbol, converters: :all).collect do |row|
     Hash[row.collect { |c, r| [c, r] }]
   end
 end

 def where property, expected
   @conditions[property] = expected
 end

 def data
   results = @data.dup
   @conditions.each do |key, value|
     results = results.find_all do |row|
       row[key].to_s == value
     end
   end
   results
 end

 def flush
   @conditions = {}
 end
end

This can be used just like any other Ruby object:

wc_dsl.where(:country, "Argentina")
Wc_dsl.data #returns all of the rows where the country is Argentina

Step 2: Implementing method_missing

Method missing is the first technique we will use to build our DSL, so paste the following into an IRB console:

class MyClass 
end

MyClass.new.does_not_exist

You will see the following error:

NoMethodError: undefined method `does_not_exist' for MyClass:0x007fe48e873618
    from (irb):6

We’ll now override the default behavior by implementing method_missing to bring the error up on the screen:

class MyClass 
  def method_missing(m, *args, block)
    puts "called method #{m}, but it does not exist"
  end

end

MyClass.new.does_not_exist

Now that we’ve changed the default behavior, we can leverage this to create our Ruby DSL. Add the following to the ç class in the world_cup_dsl.rb file:

def method_missing(m, *args, block)
 where(m, args.first)
end

We can now run the WorldCupDSL as such:

wc_dsl = WorldCupDSL.new 'data.csv'
wc_dsl.country "Argentina"
wc_dsl.data #returns all of the rows where the country is Argentina

As you can see, we can now use a method called country, or any other row heading in the CSV, such as position or last_name, to filter the data by column value.

The full code for this part can be seen here.

Step 3: Implementing instance_eval

A proc in Ruby is Ruby code that is declared in a block and assigned to a variable to be used at a later time. Try the following in IRB:

p= Proc.new {|arg|puts arg}
p.call “input”

Now let us change the implementation to use a method call foo instead of puts:

p= Proc.new {|arg|foo arg}
p.call “input”

You will get an error: NoMethodError: undefined method `foo’ for main:Object.

This is where our second metaprogramming technique will be used. We will use instance_eval to call this proc, but in a different way, with an object that has the foo method.

class MyClass
 def foo arg
   puts 'foo ' + arg
 end
end

p = Proc.new do
 foo 'Something'
end

MyClass.new.instance_eval p

By using instance_eval, the code block is run in a different way where the foo method exists.

We can now apply the same technique to our WorldCupDSL:

def query block
 instance_eval(block)
 data
end

With this extra functionality, we can now execute WorldCupDSL in a way that is similar to RSpec:

wc_dsl = WorldCupDSL.new 'data.csv'
wc_dsl.query do
 country "Argentina"
 height '175'
end

You can see the final product here

Leave a Reply

Your email address will not be published. Required fields are marked *

*
*

cover letter