How to create a DSL with Groovy

Christoph Dähne10.07.2015

This article describes one way to create an own "Domain Specific Language" (DSL) in Java/Groovy. A DSL is a programming language designed to solve a specific issue very efficiently. For a more elaborated explanation I recommend the article on Wikipedia.

By definition a DSL can be applied to many areas. To make this article easier to follow, I limit its scope a bit. Nonetheless the principles and examples are similar in other areas.

DSL support in Groovy

Writing an own programming language seems like a lot of tedious work. Fortunately this is not the case using Groovy as a base.

A Groovy-based DSL is compiled by the groovy compiler. So there is nothing to do for that other than that the DSL must share syntax with Groovy. The pre-compilation of DSL code and its compilation at runtime are supported as well, though this article covers only the pre-compilation case.

DSL for bootstrapping and configuration

In several projects we use DSLs for very advanced configuration and for the bootstrapping of the application. Usually this kind of configuration is written by the application developers rather than the customer.

An open source Webserver named Ratpack uses the same approach. Check out the Ratpack configuration DSL.

The DSL-configurations are very compact and readable and fit the task 100% (otherwise we would update the DSL). Still adding new keywords to the DSL is rather simple.

Hello World

To execute the examples you can copy-paste them into the GroovyConsole.

class HelloWorldService { // this function becomes a keyword in our DSL void printHelloWorld() { println("Hello World") } } class HelloWorldScript { static void execute(Closure script) { // here we wire the colure with the service script.delegate = new HelloWorldService() // search the delegate for variables and methods // if they do not exist search the scope of the closures definition script.resolveStrategy = Closure.DELEGATE_FIRST // the closure is executed in the context of its delegate script() } } HelloWorldScript.execute { printHelloWorld() for(def i = 0; i < 10; i++) { printHelloWorld() } }

Output

Hello World
...
Hello World

Hello World with syntax highlighting

IDEAs with good Groovy support provide syntax highlighting and auto-completion for DSLs. To give a hint to the IDEA on which delegate a closure is executed, a special annotation exists: @DelegatesTo. The annotation has no additional effect.

class HelloWorldService { void printHelloWorld() { println("Hello World") } } class HelloWorldScript { // by annotating the parameter we gain syntax highlighting and auto-completion static void execute(@DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = HelloWorldService) Closure script) { script.resolveStrategy = Closure.DELEGATE_FIRST script.delegate = new HelloWorldService() script() } } HelloWorldScript.execute { printHelloWorld() for(def i = 0; i < 10; i++) { printHelloWorld() } }

Bouquets

Usually we want the DSL to have a return value rather than print to standard out, e.g. a model containing the application's configuration.

// our configuration model class BouquetConfiguration { private final List<String> flowers = [] // method for use at runtime int howMany(String flower) { return flowers.count { it == flower } } // DSL keyword for use at DSL execution void flower(String flower) { flowers << flower } String toString() { return "Bouquet: " + flowers.toString() } } class Bouquet { static BouquetConfiguration create( @DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = BouquetConfiguration) Closure script ) { def bouquet = new BouquetConfiguration() script.resolveStrategy = Closure.DELEGATE_FIRST script.delegate = bouquet script() return bouquet } } def bouquet = Bouquet.create { flower "cornflower" flower "cornflower" flower "poppy" flower "calendula" } println bouquet

Output

Bouquet: [cornflower, cornflower, poppy, calendula]

Bouquets even better

The prior Bouquet-DSL has a bad touch: the BouquetConfiguration serves two purposes. On the one hand it serves as data model and on the other it implements keywords for our DSL. In this small example it does not seem like a big deal, but mixing those concerns leads to problems in more complex situations:

  • it is harder to decide which method can be used in which context
  • it messes up the auto-completion 
  • it pins the DSL to this model

In theory the DSL can be executed in different ways, e.g. to create mocks during test setups.

Long story short: I recommend to keep the model and the DSL separate.

// our configuration model class BouquetConfiguration extends ArrayList<String> { int howMany(String flower) { return this.count { it == flower } } String toString() { return "Bouquet: " + this.toString() } } // our DSL keyword class BouquetConfigurationDsl { private final BouquetConfiguration bouquet BouquetConfigurationDsl(BouquetConfiguration bouquet) { this.bouquet = bouquet } void flower(String flower) { bouquet << flower } } class Bouquet { static BouquetConfiguration create( @DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = BouquetConfigurationDsl) Closure script ) { def bouquet = new BouquetConfiguration() script.resolveStrategy = Closure.DELEGATE_FIRST script.delegate = new BouquetConfigurationDsl(bouquet) script() return bouquet } } def bouquet = Bouquet.create { flower "cornflower" flower "cornflower" flower "poppy" flower "calendula" } println bouquet

Output

Bouquet: [cornflower, cornflower, poppy, calendula]

Florist with nesting

Now let's extend the Bouquet-example to create florists offering different bouquets. We want to nest DSL-blocks inside of other DSL-blocks, in our case Bouquet-configuration inside Florist-configuration.

class BouquetConfiguration extends ArrayList<String> { int howMany(String flower) { return this.count { it == flower } } String toString() { return "Bouquet: " + this.toString() } } class FloristConfiguration { final Map<String, BouquetConfiguration> catalog = [:] String toString() { return "Florist: " + catalog.toString() } } class BouquetConfigurationDsl { private final BouquetConfiguration bouquet BouquetConfigurationDsl(BouquetConfiguration bouquet) { this.bouquet = bouquet } void flower(String flower) { bouquet << flower } } class FloristConfigurationDsl { private final FloristConfiguration florist FloristConfigurationDsl(FloristConfiguration florist) { this.florist = florist } BouquetConfiguration bouquet( String name, @DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = BouquetConfigurationDsl) Closure script ) { def bouquet = florist.catalog[name] if (bouquet == null) { bouquet = florist.catalog[name] = new BouquetConfiguration() } script.resolveStrategy = Closure.DELEGATE_FIRST script.delegate = new BouquetConfigurationDsl(bouquet) script() return bouquet } } class Florist { static FloristConfiguration create( @DelegatesTo(strategy = Closure.DELEGATE_FIRST, value = FloristConfigurationDsl) Closure script ) { def florist = new FloristConfiguration() script.resolveStrategy = Closure.DELEGATE_FIRST script.delegate = new FloristConfigurationDsl(florist) script() return florist } } def florist = Florist.create { bouquet "meadow", { flower "cornflower" flower "cornflower" flower "poppy" flower "calendula" } bouquet "roses", { flower "rose" flower "rose" flower "rose" flower "rose" } // add another calendula to existing bouquet bouquet "meadow", { flower "calendula" } } println florist

Output

Florist: [madow:[cornflower, cornflower, poppy, calendula, calendula], roses:[rose, rose, rose, rose]]

Conclusion
 

Our past experience with DSLs for bootstrapping and advanced configuration has been very good. Personally I think the benefits outweigh the drawbacks when creating a DSL based on Groovy.

Benefits

  • a DSL can make code very compact and readable
  • very convenient for complex configuration
  • changes to model not necessarily invalidate DSL (stable API)
  • easy and quick to implement

Drawbacks

  • Groovy syntax is mandatory
  • script can contain any Java/Groovy code => not wise to let any user upload a script

Let us know your experiences with DSLs or any comments you have on Twitter!

Dein Besuch auf unserer Website produziert laut der Messung auf websitecarbon.com nur 0,28 g CO₂.