| layout | default |
|---|---|
| title | Groovy DSL |
| edit | false |
| buttons | none |
A Domain Specific Language (DSL) is a computer programming language specialized to a specific application domain. [1]
The Language Applications Grid uses several domain specific languages:
LSD is basically just Groovy bundled with the LAPPS API modules that are automatically imported into user scripts. The other three can be thought of executable configuration languages. The advantage of a DSL over a data format like Yaml or JSON is that it is executable code that can define functions, store commonly used values in variables, and make use of flow of control and looping statements.
While the syntax may look odd at times, every DSL is a syntactically correct Groovy program. So even though the DSL may look like a configuration file it will actually be compiled in to Java bytecode and executed on the JVM. Consider a statement from the Discriminator DSL:
token {
uri 'http://vocab.lappsgrid.org/Token'
description 'A string of one or more characters that serves as an indivisible unit for the purposes of morpho-syntactic labeling (part of speech tagging).
}The above calls a method named token that takes a closure as its only parameter:
public void token(Closure body) { ... }The closure contains two statements. The first statement calls the method uri with a single String parameter and the second statement calls the method description with a single String parameter.
The GroovyShell class makes it easy to compile a String into code that can be executed.
String code = 'println "hello world"'
def shell = new GroovyShell()
def script = shell.parse(code)
script.run()
// prints hello worldGroovy allows us to customize the compiler so we can do things like:
- Add import statements to the script
- Set a base class for the script
- Provide a Binding object to the script to inject variables.
Groovy provides several metaprogramming techniques that assist in writing a DSL. Two of the most useful are:
methodMissing(String,List)allows us to synthesize methods dynamically. This is a powerful technique for creating Builders, that is, objects that build other objects based on some sort of description.- Closure delegates. A delegate is like a super class for a closure. If Groovy can not resolve a method call in the closure's scope it will look in the delegate for the method.
{{ site.top }}
Suppose we want to transform the following simplified Discriminator DSL into a JSON representation.
token {
uri 'http://vocab.lappsgrid.org/Token'
description 'A string of one or more characters that serves as an indivisible unit.'
}
sentence {
uri 'http://vocab.lappsgrid.org/Sentence'
description 'A sequence of one or more words.'
}Our DSL processor will do three things:
- Implement a delegate class that provides the
urianddescriptionmethods. - Parse the code into a Script object.
- Implement
script.metaClass.methodMissingto intercept method calls. - Generate the JSON from the constructed data structure.
The Delegate class will provide the uri and description methods. Each method takes a String and will save it in a field of the same name.
class Delegate {
String uri
String description
void uri(String uri) { this.uri = uri }
void description(String description) { this.description = description }
}We have already seen how to do this above.
Script script = new GroovyShell().parse(code)def objects= [:]
script.metaClass.methodMissing = { String name, args ->
Closure cl = args[0]
cl.delegate = new Delegate()
cl();
objects[name] = cl.delegate
}First we create a HashMap to hold the generated objects. We assume that the first parameter to the missing method will always be a closure, but in practice we would need to so some error checking here. After running the closure we add the Delegate object to the map. In practice we would do some error checking here as well to make sure any required fields in the delegate were initialized and that fields were initialized with proper values.
Groovy's JsonBuilder class makes the final step trivial.
println new groovy.json.JsonBuilder(objects).toPrettyString()Putting it all together the complete DSL processor looks like:
class Dsl {
public static void main(String[] params)
{
String code = new File(params[0]).text
def objects = [:]
Script script = new GroovyShell().parse(code)
script.metaClass.methodMissing = { String name, args ->
Closure cl = args[0]
cl.delegate = new Delegate()
cl();
annotations[name] = cl.delegate
}
script.run()
println new groovy.json.JsonBuilder(objects).toPrettyString()
}
}
class Delegate {
String uri
String description
void uri(String uri) { this.uri = uri }
void description(String description) { this.description = description }
}If we did not implement script.metaClass.methodMissing then Groovy would complain about the missing method token when we tried to run the program on the example input. Similarly, if we did not add a delegate that implemented uri and description to the closure Groovy would complain about the missing method uri when attempting to execute the closure.
{{ site.top }}