Ruby DSLs for fun & profit

Using a DSL (domain specific language) is an excellent way to improve your codebase's readability and ensure it can be extended easily. I'm very much a fan of creating DSLs within my apps & tools to ensure other developers can easily work with the APIs I have put in place for them. In this blog post, I'm going to explore how to create a simple DSL in Ruby.

Getting started

We're going to be creating a DSL which allows you to define a basic address book in Ruby. Here's what the final DSL will look like for each contact in our address book.

contact do

  name 'Joe', 'Bloggs'
  company 'Bloggs Inc'

  phone :home, '01234 123123'
  phone :work, '01555 123555'
  email :home, "joe@bloggs.com"

  activities << 'Piano Playing'
  activities << 'Swag Catching'

end  

As you can see, we're defining a contact who has a name, company, contact details and a number of favourite activities. But... how would we access our address book using Ruby?

# Initialize a new address book
address_book = AddressBook.new

# Calling contacts will return an array of all the contacts you've defined.
address_book.contacts   #=> [AddressBook::Contact, AddressBook::Contact, ...]

# Looking at an individual contact
contact = address_book.contacts.first  
contact.first_name        #=> 'Joe'  
contact.last_name         #=> 'Bloggs'  
contact.contact_methods   #=> [AddressBook::ContactMethod, ...]  
contact.activites         #=> ['Piano Playing', 'Swag Catching']  

This is only a very basic set of data but you can use the same techniques I'm about to describe to create more complicated DSLs.

Creating our base classes

To begin, we're going to create our AddressBook class which will be the first and root-level object we instantiate. For now, an address book will simply stores an array of all the contacts which are within it.

class AddressBook

  def contacts
    @contacts ||= []
  end

end  

As you can see, we've created a class with a method named contacts. This will return an array of contacts. If you haven't seen the ||= operator before, this will set the value of @contacts to an empty array if it is null when the method is called.

We're also now going to go ahead and define our Contact class. Every contact we define will be stored within an instance of this.

class Contact

  # Define the basic information attributes
  attr_accessor :first_name
  attr_accessor :last_name
  attr_accessor :company

  # Define a method to return an array of contact methods for
  # this contact. 
  def contact_methods
    @contact_methods ||= []
  end

  # Define a method to return an array of activities for the 
  # this contact.
  def activities
    @activities ||= []
  end

end  

Finally, we're going to create a ContactMethod class which will contain information about each contact method a contact has. For example, each phone number and email address along with their roles (home, work, etc...).

class ContactMethod

  # When we create a new contact method, we'll store the contact
  # which it is associated with too.
  def initialize(contact)
    @contact = contact
  end

  # Accessor for returning a contact associated with a ContactMethod
  attr_reader :contact

  # Accessors for getting & setting the various properties which
  # are associated with a ContactMethod.
  attr_accessor :type
  attr_accessor :role
  attr_accessor :value

end  

Creating our DSL

That's all well and good and we could use that today by simply creating instances of these objects like shown below:

address_book = AddressBook.new  
contact = Contact.new  
contact.first_name = 'Adam'  
contact.last_name = 'Bloggs'  
contact.company = 'Acme Inc'  
contact.activities << 'Fly Fishing'

contact_method = ContactMethod.new(contact)  
contact_method.type = :email  
contact_method.role = :home  
contact_method.value = "adam@acmeinc.com"  
contact.contact_methods << contact_method  

This is a very verbose way to do this and requires an in-depth knowledge of Ruby and the backend structures. To simplify this, we're going to create our DSL. We're going to assume that our contacts are simply all defined in a simple flat file (exactly as shown above).

If we were to execute this file, normally it would just fail saying that the contact method did not exist so we can fix that by defining that method. I do this by defining special classes for each of my objets and then executing my DSL-ified files within an instance of that class.

I'm going to start by creating a class which the address book files can be be interpreted through. As our address book just stores contacts, we only need to define a contact method.

class AddressBookDSL

  def initialize(address_book)
    @address_book = address_book
  end

  def contact(&block)
    # Create a new Contact object instance for our new contact
    contact = Contact.new
    # Create a new instance of our Contact DSL (defined in a moment)
    # and pass it our newly instantiated contact.
    contact_dsl = ContactDSL.new(contact)
    # Eval the contacts of the `contact` block within the ContactDSL
    # instance we just created.
    contact_dsl.instance_eval(&block)
    # Add the contact to the array of contacts in our address book
    @address_book.contacts << contact
  end

end  

Quite a bit happened just there. The most important thing here is the instance_eval. This method evaluates the block passed to it within the context of class on which it was called (in this case, the ContactDSL). This will therefore provide the block with access to the classes instance variables & instance methods.

Creating a DSL for contacts

Now we understand that, the rest is pretty simple. We're going to go ahead and create a new DSL class for our contacts.

class ContactDSL

  def initialize(contact)
    @contact = contact
  end

  # When the name method is called, we'll accept the first and
  # last names and set them as appropriate on our contact class.
  def name(first_name, last_name)
    @contact.first_name = first_name
    @contact.last_name = last_name
  end

  # When the company is set, we'll just do the same and set that
  # on the contact.
  def company(name)
    @contact.company = name
  end

  # Activities are just passed straight through from the contact
  # itself so we just return the contact's activities array.
  def activities
    @contact.activities
  end

  # When a contact method is defined, we'll need to create a new
  # instance of our ContactMethod class, set the values and then
  # add it to our contact's contact_method array.
  def contact_method(type, role, value)
    method = ContactMethod.new(@contact)
    method.type = type
    method.role = role
    method.value = value
    @contact.contact_methods << method
  end

end  

This class is pretty simple but it won't work quite yet. In our example earlier, we used phone and email methods to add contact methods to our contact. At the moment, this class just supports adding methods using contact_method which is a little cumbersome.

As the number of possible contact methods could be quite large, we don't really want to define methods on our DSL for every possible option. Therefore, we're going to use Ruby's method_missing function. If this method is defined in our class, Ruby will call it whenever a call is made to a method which doesn't exist. As email and phone haven't been defined, calling these will invoke this method.

# This is defined to accept name (which is the name of the method which
# has been called) and also any other number of values which are passed
# to it (using the splat operator). 
def method_missing(name, *values)  
  if values.empty?
    # If no values are provided, we'll just let Ruby raise an error
    # as normal.
    super
  else  
    # Otherwise, we'll simply call our contact_method method to 
    # add our method.
    contact_method(name, *values)
  end
end  

Loading our address book

That's about it for defining our DSLs and all we need to do now is load our address book files into our application. To do this, we'll just add a new class method to our AddressBook class as shown below.

def self.load_from_path(path)  
  # Create a new AddressBook instance
  address_book = self.new
  # Create a new AddressBookDSL instance
  address_book_dsl = AddressBookDSL.new(address_book)
  # Loop through every file in the given path
  Dir[File.join(path, '*')].each do |file|
    # Read each file and eval the contents in the address
    # book's DSL instance.
    address_book_dsl.instance_eval(File.read(file), file)
  end
  # Return the finished address book
  address_book
end  

To load up your address book, you can now simply create a folder of files containing your contacts and then run:

address_book = AddressBook.load_from_path('my/contacts/folder')  
address_book.contacts.each do |contact|  
  puts "#{contact.first_name} #{contact.last_name}"
  puts contact.company
end  

Final thoughts

That's about it, folks! It's not too complicated and I hope this post has provided some enlightenment about how to go about this.

You can use a DSL in a number of ways to improve your application. The biggest DSL I've created recently is Moonrope which uses the techniques in this blog post to build a DSL for creating a complete API service. A smaller utility is my VAT Rates repository which stores the VAT rates for all EU countries in a simple DSL so that anyone can submit pull requests to the rates without worrying too much about the Ruby side of things.

If you want to play with this example a bit further, I've posted all the code into a single file which you can just download and run on your own computer.

I'd love to hear about things you've created so feel free to drop me a message on twitter (@adamcooke).