Groovy DSL – A Simple Example
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.
1 2 3 4 5 6 | 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.
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:
1 2 3 4 5 6 7 8 | 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:
1 2 3 4 5 6 7 | <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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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.
1 2 3 4 5 6 7 8 | 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.
1 2 3 4 5 6 7 8 | /** * 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /** * '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.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | 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 } |
Reference: Groovy DSL – A Simple Example from our JCG partner Nirav Assar at the Assar Java Consulting blog.
You might like GroovyPoint app to run your Groovy code. Especially when you are learning it… https://itunes.apple.com/us/app/groovypoint/id1038117543?mt=12