Skip to main content

Build your own Domain-Specific Language with Ruby

·4 mins

“So, what is your favourite design pattern?” As odd as it sounds, it is a common question between fellow engineers and in job interviews.

But aren’t we supposed to weigh our options according to our application logic and pick a suitable one? Leaving that aside, my answer would probably be internal DSLs.

Arguably, one of the most famous books written on this subject is Design Patterns: Elements of Reusable Object-Oriented Software by the ****"Gang of Four". However, you won’t find DSLs in this book, and which is why exactly it is interesting. Most of the original design patterns described in this book were developed without Ruby in mind. By leveraging Ruby’s features, DSL provides a way to implement our own little language.


When writing Ruby code, we use internal DSLs quite often. Rake, RSpec, ActiveRecord and Sinatra come with their own internal DSL. Let’s examine two common testing libraries among Rubyists: minitest and RSpec.

# minitest sample
class PlaneTest < MiniTest::Test
  .
  .
  .
  def test_engines
    assert_equal(4, Plane.engines.length)
  end
end

# RSpec sample
describe PlaneTest do
	.
	.
	.
  it "should have 4 engines" do
   expect(Plane.engines.length).to eq(4)
  end
end

The former minitest example uses pure Ruby*,* our PlaneTest class inherits from MiniTest::Test and we create instance methods for each test. On the RSpec example, we use the two methods describe and it for the same purposes. Under the hood, RSpec uses a DSL to allow this syntax that looks like plain English.

Simplistic DSL syntax is enabled by a couple of features in Ruby:

  • Omitting parentheses on method invocations:
load("http://example.com/pictures", "my/dir/local")
load "http://example.com/pictures", "my/dir/local"
  • eval method evaluates the argument and runs the content as Ruby program text.
eval("1 + 1") #=> 2
  • instance_eval will go one step further and change the context(self) to the object that instance_evalis being called on.
class Universe
	attr_reader :secret
	def initialize
		@secret = 42
	end
end

uni = Universe.new
uni.instance_eval { secret } #=> 42

Now that we have covered the basic principles, let’s implement our own little JSON parser that iterates over a collection of objects that have similar structure and extracts a Ruby array with the selected properties. It can be useful when dealing with large JSON files. The below example is from Yahoo’s woeid locations collection:

[
  {
    "name": "Birmingham",
    "placeType": {
      "code": 7,
      "name": "Town"
    },
    "url": "http:\/\/where.yahooapis.com\/v1\/place\/12723",
    "parentid": 23424975,
    "country": "United Kingdom",
    "woeid": 12723,
    "countryCode": "GB"
  },
  {
    "name": "Bournemouth",
    "placeType": {
      "code": 7,
      "name": "Town"
    },
    "url": "http:\/\/where.yahooapis.com\/v1\/place\/13383",
    "parentid": 23424975,
    "country": "United Kingdom",
    "woeid": 13383,
    "countryCode": "GB"
  },
	.
	.
	and more
	.
	.
]

Our JSONParser class with on_path and extract methods to setup the parser:

class JSONParser
  attr_reader :data
  def initialize(&block)
    @content_arr = []
    @data = []
    block.call(self) if block_given?
  end

  def on_path(file_path)
    file = File.read(file_path)
    @content_arr = JSON.parse(file)
  end

  def extract(source_key, target_key=source_key)
    unless @data.length > 0
      @content_arr.length.times { |_| @data << {} }
    end

    @content_arr.each_with_index { |json_obj, i| @data[i][target_key] = json_obj[source_key] }
  end
end

We can pass in a block with the relevant method calls while creating a new JSONParser:

parser = JSONParser.new do |parser|
  parser.on_path('gb_woeid.json')
  parser.extract("name")
  parser.extract("woeid")
  parser.extract("country")
  parser.extract("countryCode", "country_code")
end

# Test the data
puts parser.data

# Prints
{"name"=>"Birmingham", "woeid"=>12723, "country"=>"United Kingdom", "country_code"=>"GB"}
{"name"=>"Blackpool", "woeid"=>12903, "country"=>"United Kingdom", "country_code"=>"GB"}
.
.
.

We can use instance_eval to get rid of the need of calling methods on our parser object by changing our initialize method:

def initialize(&block)
    @content_arr = []
    @data = []
    # block.call(self) if block_given?
    # instance eval changes the value of self as it executes the block
    instance_eval(&block) if block_given?
  end

With our new initialize and by omitting parentheses our parser is much more DSL-like now:

parser = JSONParser.new do
  on_path "gb_woeid.json"
  extract "name"
  extract "woeid"
  extract "country"
  extract "countryCode", "country_code"
end

DSLs are great when dealing with an isolated problem repeatedly. Now that we’ve developed one of our own, we know what is going on in the background when we use one.

For more on Ruby and DSLs: