Alberto Grespan

Writing a DSL in Ruby

— December 29, 2014

A Domain Specific Language or DSL is a mini language focused in solving a particular type of problem. That said, it’s not a general purpose language like Ruby. Writing a DSL can help us improve the code base by making it more readable.

If you’ve used Rails, you’ve used and seen tons of DSLs. e.g inside migrations, configuration files etc… DSLs in Ruby are a common thing and we are making a simple but useful example in this post.

What we want

At the end of the post we will end up with an address_book object with that contains an array of contacts, all done with our simple DSL:

address_book = AddressBook.new do
  add_contact do
    full_name "Alberto Grespan"
    email "[email protected]"
  end

  add_contact do
    full_name "John Doe"
    email "[email protected]"
  end
end

Let’s start with the contact class.

Contact

We want to save the contacts in our address book with their full name and email.

class Contact
  attr_accessor :full_name, :email

  def initialize(&block)
    (block.arity < 1 ? (instance_eval &block) : block.call(self)) if block_given?
  end

  def full_name(full_name=nil)
    full_name.nil? ? @full_name : @full_name = full_name
  end

  def email(email=nil)
    email.nil? ? @email : @email = email
  end
end

What we did here is very minimal and simple, and it will work for the example used above and also with local block variables. Let me explain this a bit.

When we instantiate a new Contact object and pass it a block, it checks the block.arity, if it’s less than one it evaluates the block using instance_eval, if it’s more than one it uses block.call(self) this allow us to use the block with either a local block variable or without it.

Let’s try it out:

contact = Contact.new do
  full_name "Alberto Grespan"
  email "[email protected]"
end
#=> #<Contact:0x007fa821b240c8 @email="[email protected]", @full_name="Alberto Grespan">

Or with local variables

contact = Contact.new do |contact|
  contact.full_name "Alberto Grespan"
  contact.email "[email protected]"
end
#=> #<Contact:0x007fa821c25bc0 @email="[email protected]", @full_name="Alberto Grespan">

Now we need to wrap the Contact class functionality inside an AdressBook to match our desired goal.

AddressBook

The AddressBook class is pretty straight forward. It should be able to manage an array of contacts and have a method named add_contact that receives a block and appends a new Contact to the contacts array.

class AddressBook
  attr_accessor :contacts

  def initialize(&block)
    @contacts = []
    (block.arity < 1 ? (instance_eval &block) : block.call(self)) if block_given?
  end

  def add_contact(&block)
    @contacts << Contact.new(&block)
  end
end

In the same way we did with the Contact class we are using the block.arity, instance_eval and block.call(self) on the AddressBook class. Now we can wrap the Contact class functionality inside the add_contact method and have our AddressBook object with contacts.

Inside irb or pry, require the two classes(Contacts and AddressBook) to use the DSL:

address_book = AddressBook.new do
  add_contact do
    full_name "Alberto Grespan"
    email "[email protected]"
  end

  add_contact do
    full_name "John Doe"
    email "[email protected]"
  end
end
#=> #<AddressBook:0x007fa4eb10cee8 @contact=[
    #<Contact:0x007fa4eb10cdd0 @email="[email protected]", @full_name="Alberto Grespan">,
    #<Contact:0x007fa4eb10cce0 @email="[email protected]", @full_name="John Doe">]>

Or with local block variable

address_book = AddressBook.new do |contact|
  contact.add_contact do
    full_name "Alberto Grespan"
    email "[email protected]"
  end

  contact.add_contact do
    full_name "John Doe"
    email "[email protected]"
  end
end
#=> #<AddressBook:0x007fa4ec8dcf50 @contact=[
    #<Contact:0x007fa4ec8dce60 @email="[email protected]", @full_name="Alberto Grespan">,
    #<Contact:0x007fa4ec8dcd70 @email="[email protected]", @full_name="John Doe">]>

Keep in mind that this code can be improved performance wise and it’s just a way to make a DSL in Ruby, I also hope this is useful and simple enough to understand.

Thanks for reading and thanks to Diorman Colmenares for the help!