Analytics

Monday, January 16, 2012

Groovy DSL - A Simple Example

Domain Specific Languages (DSLs) have become a valuable part of the Groovy idiom. DSLs are used in native Groovy builders, Grails and GORM, and testing frameworks. To a developer, DSLs are consumable and understandable, which makes implementation more fluid as compared to traditional programming. But how is a DSL implemented? How does it work behind the scenes? This article will demonstrate a simple DSL that can get a developer kick started on the basic concepts.

What is a DSL

DSL's are meant to target a particular type of problem. They are short expressive means of programming that fit well in a narrow context. For example with GORM, you can express hibernate mapping with a DSL rather than XML.
  static mapping = {
    table 'person'
    columns {
      name column:'name'
    }
  }  

Much of the theory for DSLs and the benefits they deliver are well documented. Refer to these sources as a starting point:

A Simple DSL Example in Groovy

The following example offers a simplified view of implementing an internal DSL. Frameworks have much more advanced methods of creating a DSL. However this example does highlight closure delegation and meta object protocol concepts that are essential to understanding the inner workings of a DSL.

Requirements Overview

Imagine that a customer needs a memo generator. The memos needs to have a few simple fields, such as "to", "from", and "body". The memo can also have sections such as "Summary" or "Important." The summary fields are dynamic and can be anything on demand. In addition, the memo needs to be outputed in three formats: xml, html, and text.

We elect to implement this as a DSL in Groovy. The DSL result looks like this:
MemoDsl.make {
    to "Nirav Assar"
    from "Barack Obama"
    body "How are things? We are doing well. Take care"
    idea "The economy is key"
    request "Please vote for me"
    xml
}
The output from the code yields:
 <memo>
  <to>Nirav Assar</to>
  <from>Barack Obama</from>
  <body>How are things? We are doing well. Take care</body>
  <idea>The economy is key</idea>
  <request>Please vote for me</request>
  </memo>  
The last line in the DSL can also be changed to 'html' or 'text'. This affects the output format.

Implementation

A static method that accepts a closure is a hassle free way to implement a DSL. In the memo example, the class MemoDsl has a make method. It creates an instance and delegates all calls in the closure to the instance. This is the mechanism where the "to", and "from" sections end up executing methods inside the MemoDsl class. Once the to() method is called, we store the text in the instance for formatting later on.
class MemoDsl {

    String toText
    String fromText
    String body
    def sections = []

    /**
     * This method accepts a closure which is essentially the DSL. Delegate the 
     * closure methods to
     * the DSL class so the calls can be processed
     */
    def static make(closure) {
        MemoDsl memoDsl = new MemoDsl()
        // any method called in closure will be delegated to the memoDsl class
        closure.delegate = memoDsl
        closure()
    }

    /**
     * Store the parameter as a variable and use it later to output a memo
     */
    def to(String toText) {
        this.toText = toText
    }

    def from(String fromText) {
        this.fromText = fromText
    }

    def body(String bodyText) {
        this.body = bodyText
    }
}  
Dynamic Sections

When the closure includes a method that is not present in the MemoDsl class, groovy identifies it as a missing method. With Groovy's meta object protocol, the methodMissing interface on the class is invoked. This is how we handle sections for the memo. In the client code above we have entries for idea and request.
MemoDsl.make {
    to "Nirav Assar"
    from "Barack Obama"
    body "How are things? We are doing well. Take care"
    idea "The economy is key"
    request "Please vote for me"
    xml
} 
The sections are processed with the following code in MemoDsl. It creates a section class and appends it to a list in the instance.
/**
 * When a method is not recognized, assume it is a title for a new section. Create a simple
 * object that contains the method name and the parameter which is the body.
 */
def methodMissing(String methodName, args) {
 def section = new Section(title: methodName, body: args[0])
 sections << section
}
Processing Various Outputs

Finally the most interesting part of the DSL is how we process the various outputs. The final line in the closure specifies the output desired. When a closure contains a string such as "xml" with no parameters, groovy assumes this is a 'getter' method. Thus we need to implement 'getXml()' to catch the delegation execution:
/**
 * 'get' methods get called from the dsl by convention. Due to groovy closure delegation,
 * we had to place MarkUpBuilder and StringWrite code in a static method as the delegate of the closure
 * did not have access to the system.out
 */
def getXml() {
 doXml(this)
}

/**
 * Use markupBuilder to create a customer xml output
 */
private static doXml(MemoDsl memoDsl) {
 def writer = new StringWriter()
 def xml = new MarkupBuilder(writer)
 xml.memo() {
  to(memoDsl.toText)
  from(memoDsl.fromText)
  body(memoDsl.body)
  // cycle through the stored section objects to create an xml tag
  for (s in memoDsl.sections) {
   "$s.title"(s.body)
  }
 }
 println writer
}
The code for html and text is quite similar. The only variation is how the output is formatted.

Entire Code

The code in its entirety is displayed next. The best approach I found was to design the DSL client code and the specified formats first, then tackle the implementation. I used TDD and JUnit to drive my implementation. Note that I did not go the extra mile to do asserts on the system output in the tests, although this could be easily enhanced to do so. The code is fully executable inside any IDE. Run the various tests to view the DSL output.
package com.solutionsfit.dsl.memotemplate

class MemolDslTest extends GroovyTestCase {

    void testDslUsage_outputXml() {
        MemoDsl.make {
            to "Nirav Assar"
            from "Barack Obama"
            body "How are things? We are doing well. Take care"
            idea "The economy is key"
            request "Please vote for me"
            xml
        }
    }

    void testDslUsage_outputHtml() {
        MemoDsl.make {
            to "Nirav Assar"
            from "Barack Obama"
            body "How are things? We are doing well. Take care"
            idea "The economy is key"
            request "Please vote for me"
            html
        }
    }

    void testDslUsage_outputText() {
        MemoDsl.make {
            to "Nirav Assar"
            from "Barack Obama"
            body "How are things? We are doing well. Take care"
            idea "The economy is key"
            request "Please vote for me"
            text
        }
    }
}

package com.solutionsfit.dsl.memotemplate

import groovy.xml.MarkupBuilder

/**
 * Processes a simple DSL to create various formats of a memo: xml, html, and text
 */
class MemoDsl {

    String toText
    String fromText
    String body
    def sections = []

    /**
     * This method accepts a closure which is essentially the DSL. Delegate the closure methods to
     * the DSL class so the calls can be processed
     */
    def static make(closure) {
        MemoDsl memoDsl = new MemoDsl()
        // any method called in closure will be delegated to the memoDsl class
        closure.delegate = memoDsl
        closure()
    }

    /**
     * Store the parameter as a variable and use it later to output a memo
     */
    def to(String toText) {
        this.toText = toText
    }

    def from(String fromText) {
        this.fromText = fromText
    }

    def body(String bodyText) {
        this.body = bodyText
    }

    /**
     * When a method is not recognized, assume it is a title for a new section. Create a simple
     * object that contains the method name and the parameter which is the body.
     */
    def methodMissing(String methodName, args) {
        def section = new Section(title: methodName, body: args[0])
        sections << section
    }

    /**
     * 'get' methods get called from the dsl by convention. Due to groovy closure delegation,
     * we had to place MarkUpBuilder and StringWrite code in a static method as the delegate of the closure
     * did not have access to the system.out
     */
    def getXml() {
        doXml(this)
    }

    def getHtml() {
        doHtml(this)
    }

    def getText() {
        doText(this)
    }

    /**
     * Use markupBuilder to create a customer xml output
     */
    private static doXml(MemoDsl memoDsl) {
        def writer = new StringWriter()
        def xml = new MarkupBuilder(writer)
        xml.memo() {
            to(memoDsl.toText)
            from(memoDsl.fromText)
            body(memoDsl.body)
            // cycle through the stored section objects to create an xml tag
            for (s in memoDsl.sections) {
                "$s.title"(s.body)
            }
        }
        println writer
    }

    /**
     * Use markupBuilder to create an html xml output
     */
    private static doHtml(MemoDsl memoDsl) {
        def writer = new StringWriter()
        def xml = new MarkupBuilder(writer)
        xml.html() {
            head {
                title("Memo")
            }
            body {
                h1("Memo")
                h3("To: ${memoDsl.toText}")
                h3("From: ${memoDsl.fromText}")
                p(memoDsl.body)
                 // cycle through the stored section objects and create uppercase/bold section with body
                for (s in memoDsl.sections) {
                    p {
                        b(s.title.toUpperCase())
                    }
                    p(s.body)
                }
            }
        }
        println writer
    }

    /**
     * Use markupBuilder to create an html xml output
     */
    private static doText(MemoDsl memoDsl) {
        String template = "Memo\nTo: ${memoDsl.toText}\nFrom: ${memoDsl.fromText}\n${memoDsl.body}\n"
        def sectionStrings =""
        for (s in memoDsl.sections) {
            sectionStrings += s.title.toUpperCase() + "\n" + s.body + "\n"
        }
        template += sectionStrings
        println template
    }
}

package com.solutionsfit.dsl.memotemplate

class Section {
    String title
    String body
}

56 comments:

  1. Your code is missing a Section class:

    class Section {
    String title
    String body
    }

    ReplyDelete
  2. I think it's important to emphasize that this is a builder DSL. There are many forms of DSLs, each with different techniques.

    But good article! A getJson() method would be useful too ;) It's not clear why toText, fromText, and body are different from sections, except that maybe they are required. But they're not explicitly enforced, are they?

    ReplyDelete
  3. Does anyone know any Grails developers looking for work around Columbus, Ohio or willing to relocate to Columbus? I am desperate!

    ReplyDelete
  4. pleased update your codes

    thank

    ReplyDelete
  5. What codes do you want me to update?

    ReplyDelete
  6. Can you describe with GUI Interface?

    ReplyDelete
  7. selamet, i am not sure what you mean by this question

    ReplyDelete
  8. Assignment of closure.delegate in MemoDsl seems to be not thread safe. What happens if this code is being called from multiple threads? Will different threads attempt to change the state ("delegate" property) of the same object simultaneously? Just a question.

    ReplyDelete
  9. I imagine it is not thread safe

    ReplyDelete
  10. Very interesting, good job and thanks for sharing such a good blog. Interesting stuff to read..................
    R12 SCM Online Training

    ReplyDelete
  11. Good explanation having in this blog.detailed about online training.
    very informative .thanks for sharing this article.

    ReplyDelete
  12. Good explanation having in this blog.detailed about online training.
    very informative .thanks for sharing this article.
    oracle fusion procurement online training

    ReplyDelete
  13. wonderful information, I had come to know about your blog from my friend nandu , hyderaba.i have read atleast 7 posts of yours by now, and let me tell you, your website gives the best and the most interesting information. This is just the kind of information that i had been looking for, i'm already your rss reader now and i would regularly watch out for the new posts.

    from

    Oracle Fusion HCM Online Training

    ReplyDelete
  14. I have been trying to understand the DSL concept in Java programming in vain but I am glad that I landed on this site and found a comprehensive article on the concept and I have been able to grasp both the basic and the complex concepts of Groovy DSL. Thanks so much for sharing this article and please find time and check out my article by clicking on Quality Dissertation Editing Services.

    ReplyDelete
  15. Good explanation having in this blog.detailed about online training.very informative .thanks for sharing this article.
    Oracle Fusion Financials Online Training

    ReplyDelete
  16. Thanks for all the information, it was very helpful I really like that you are providing information................Get some great info about Fusion Cloud Financials Course.

    ReplyDelete
  17. Your given most of the usefull information, i read your post is very nice thank you....................Search for Oracle Project Accounting Training Institute details in our local search engine Calfre.

    ReplyDelete
  18. Great Work, after reading this post I felt comfortable with this post thank you very much.
    Oracle HRMS Training in Al Karama, Dubai
    click Here

    ReplyDelete
  19. This site has lots of advantage.I found many interesting things from this site. It helps me in many ways.Thanks for posting this again.
    Oracle Fusion SCM Training Institutes in Hyderabad

    ReplyDelete
  20. I found your blog while searching for the updates, I am happy to be here. Very useful content and also easily understandable providing.. Believe me I did wrote an post about tutorials for beginners with reference of your blog. 
    Devops Training courses
    python Training in chennai
    Devops Training in Bangalore

    ReplyDelete
  21. I have picked cheery a lot of useful clothes outdated of this amazing blog. I’d love to return greater than and over again. Thanks! 
    python Training institute in Chennai
    python Training institute in Bangalore
    python Training in Pune

    ReplyDelete
  22. Thanks for sharing.
    Ensures best online Job Support.
    We help IT professionals by providing them Best Online Job Support in 250+ technologies. Our services are very reliable and most affordable. Call Today for free demo.

    ReplyDelete
  23. Thanks for sharing this informative content , Great work
    Leanpitch provides online training inScrum Master during this lockdown period everyone can use it wisely.
    Advanced Scrum Master Training

    ReplyDelete
  24. thanks for sharing blog if you intersted to learn more visit it https://snowflakemasters.in/

    ReplyDelete
  25. Thanks for sharing this post with us.. good post!!!
    What is DevOps
    AWS DevOps Tools

    ReplyDelete
  26. This comment has been removed by the author.

    ReplyDelete
  27. Nice thanks for sharing informative post like this keep posting.

    Are u looking for Acne Treatment in Hyderabad
    Click here: Aira Clinics

    ReplyDelete
  28. Nice blog thanks for sharing information if like more visit this site.

    Are u looking for UCATCoaching

    ReplyDelete
  29. Great deals of important information and also a great article. I am currently following for your blog site and I am bookmarking it future reference. thanks for sharing!

    ReplyDelete
  30. bus rental Dubai full of information with great content thanks for sharing

    ReplyDelete
  31. This comment has been removed by a blog administrator. our sclinbio.com

    ReplyDelete
  32. Your post is really informative and attractive keep doing like this Bhutan National ParkThank you for sharing your good content.

    ReplyDelete
  33. Nice blog! Good explanation about Java "I'm impressed with the quality of your posts." Keep posting more
    Java full stack training institute in KPHB

    ReplyDelete
  34. "Great article, felt good after reading, worth it.
    i would like to read more from you.
    keep posting more.
    also follow Mern Stack course in hyderabad"

    ReplyDelete
  35. very nice article wonderful blog keep postingSelenium Training Institute In Hyderabad">digital marketing course in Kukatpally

    ReplyDelete
  36. شركة الركن المثالي تقدم خدمات متخصصة في صيانة ولحام خزانات المياه بكفاءة عالية وحلول مبتكرة لضمان استدامتها.
    شركة لحام خزانات
    5346859

    ReplyDelete