Build your own Domain-Specific Language with Ruby
“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.
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
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:
evalmethod evaluates the argument and runs the content as Ruby program text.
instance_evalwill go one step further and change the context(
self) to the object that
instance_evalis being called on.
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:
JSONParser class with
extract methods to setup the parser:
We can pass in a block with the relevant method calls while creating a new
We can use
instance_eval to get rid of the need of calling methods on our parser object by changing our
With our new
initialize and by omitting parentheses our parser is much more DSL-like now:
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 come across one.
For more on Ruby and DSLs: