It's been an itch to scratch lately and finally got around to understanding how to go about writing an example DSL in ruby. Machinist, sinatra, rake, cucumber were all inspirations in taking a jab at it.
Why DSL via Ruby? The language is already highly expressible as it is but with syntactic sugar, as this post will illustrate, it is so much better, easier to read, understand and operate on the code. So much so to the point for non-technical people to comprehend it. Cucumber touts it for example.
Scenario: Create an article in Academia
Let's say an article has the following attributes:
- Title
- Author(s)
- Journal
- Abstract
- Content
How about some plain old ruby to work with that?
# create a new article instance article = Academia::Article.new("Wanting to play around with ruby DSL") # authors article.authors = [ Author.new("Marge Simpson"), Author.new("Bart Simpson"), Author.new("Lisa Simpson"), Author.new("Margaret Simpson"), Author.new("Homer Simpson") ] # journal journal = Journal.new journal.name = "Springfield Scientific" journal.volume = 20 journal.issue = 3 article.journal = journal # article abstract article.abstract = %(Still doing things the old-fashioned way.) # article content article.content = %(Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. )
DSL-ifying your code can make the operation above look like the following:
article = Academia::Article.create "Writing a DSL for academia" do author "Marge Simpson" author "Bart Simpson" author "Lisa Simpson" author "Margaret Simpson" author "Homer Simpson" journal { name "Springfield Scientific" volume 20 issue 3 } abstract %(Gimme somma that syntactic sugar!!) content %(Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.) end
If you're sold on this, let's dig in a little deeper
Author is a Person subclass, hence:
module Academia class Person attr_accessor :name def initialize(name) @name = name end end class Author < Person def initialize(name) super end end end
The Journal class
module Academia class Journal attr_accessor :journal_name, :volume_number, :issue_number def name(journal_name) @journal_name = journal_name end def volume(volume_number) @volume_number = volume_number end def issue(issue_number) @issue_number = issue_number end def to_s s = "\tJournal - #{@journal_name}\n" s += "\t\tVolume - #{@volume_number}, Issue - #{@issue_number}\n" end end end
The Article class supporting DSL will look like the following below, pay special attention the instance_eval methods.
module Academia class Article attr_accessor :title, :authors, :journal, :abstract_content, :article_content # constructor def initialize(title) @title = title @authors = [] @journal = nil @abstract_content = "" @article_content = "" end # the dsl magic kicking off here below def self.create(title, &block) article = Article.new(title) article.instance_eval(&block) article end # a dsl method to store the authors def author(author_name) @authors << Author.new(author_name) end # a dsl method to create the journal instance def journal(&block) @journal = Journal.new @journal.instance_eval(&block) end # a dsl method to store the abstract text def abstract(abstract_content) @abstract_content = abstract_content end # a dsl method to store the article text def content(article_content) @article_content = article_content.gsub(/\s+/," ") end # a handy utility method to print out the article details def to_s s = "Article\n\tTitle - #{@title}\n" s += "\tAuthors -\n" @authors.each_with_index do |author, i| s += "\t\t#{i+1}: #{author.name}\n" end s += @journal.to_s s += "\tAbstract -\n\t\t#{@abstract_content}\n" s += "\tContent -\n\t\t#{@article_content}" end end end
Show blocks some love
The instance_eval method is where the magic really is taking place. The block that you passed to instance_eval got eval'd in the context of the Article and Journal instances. Ruby just makes it so wonderfully easy to work with DSL, that I hope if you see a need for it in your project you will leverage it. Cheers!