Simple Academia DSL in Ruby Tutorial

ruby dsl

Sat Nov 20 14:56:52 -0800 2010

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!

blog comments powered by Disqus