Creating a Groovy DSL for Structurizr
Groovy DSLs are easy to make, and in this example, we'll create one for the Structurizr documentation generator, showing off Groovy's Closure class along the way.
Join the DZone community and get the full member experience.
Join For FreeIn the previous post, we took a quick look into generating documentation with Structurizr. I really enjoyed playing with the tool, but I wasn’t aesthetically pleased with the code necessary to create a simple diagram. Well, seems like a perfect chance to introduce you to creating Groovy DSLs and produce something useful at the same time.
What Are We Going to Do?
We’re going to start from the very end so that you know what we’re aiming for and better understand the things that I’m going to explain. One of the code samples in the previous post looked like this:
public class HelloWorld {
private static final String API_KEY = "your-api-key";
private static final String API_SECRET = "your-api-secret";
private static final int WORKSPACE_ID = 0; // your workspace ID
public static void main(String[] args) throws Exception {
Workspace workspace = new Workspace("My First Workspace", "I'm using this to learn about Structurizr");
Model model = workspace.getModel();
ViewSet viewSet = workspace.getViews();
Person me = model.addPerson("Me", "Myself.");
SoftwareSystem world = model.addSoftwareSystem("World", "Earth, to be precise.");
me.uses(world, "Hello, World!");
viewSet.createSystemContextView(world, "My First View", "Just me and the world.").addAllElements();
StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET);
structurizrClient.putWorkspace(WORKSPACE_ID, workspace);
}
}
As you can see, there are quite a lot of objects and non-fluent method calls involved. With a little help from Groovy, we will be able to replace the code above with something like this:
class HelloWorld {
private static final String API_KEY = "your-api-key"
private static final String API_SECRET = "your-api-secret"
private static final int WORKSPACE_ID = 0 // your workspace ID
static void main(String[] args) {
def ws = Structurizr.workspace {
name "My First Workspace"
description "I'm using this to learn about Structurizr"
softwareSystem {
name "World"
description "Earth, to be precise."
}
person {
name "Me"
description "Myself."
uses {
softwareSystem "World"
description "Hello, World!"
}
}
systemContextView {
softwareSystem "World"
key "My First View"
description "Just me and the world."
}
}
StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET);
structurizrClient.putWorkspace(WORKSPACE_ID, ws);
}
}
This version is a bit longer, but that’s the price I’m willing to pay for more expressiveness and readability. Let’s do this!
Introduction to Groovy DSLs
If you don’t know Groovy well enough, the code above might seem like some sort of dark magic. But don’t worry, it should seem super easy in just a moment.
Optional Parenthesis
The first thing that you need to know in order to understand the code above is that Groovy allows you to skip parenthesis in method calls. It means that the lines...
name "My First Workspace"
description "I'm using this to learn about Structurizr"
...are equivalent in Groovy to:
name("My First Workspace")
description("I'm using this to learn about Structurizr")
Closures
The second thing that you should know about Groovy is the Closure class and the related syntax. A closure in Groovy is like a pimped out version of the Java 8 lambda expression. Similarly to Java’s lambda expressions, to define a closure, we need to use the curly braces. The difference between the two is that closure’s parameters are specified inside the braces, instead of outside. When there are no arguments, you just skip the argument name and the “arrow”.
// Java 8:
arg -> {
// do stuff
return "some value"
}
() -> {
// do stuff
return "some value"
}
// Groovy:
{ arg ->
// do stuff
return "some value"
}
{
// do stuff
return "some value"
}
Optional Parenthesis + Closures
When you combine the previous two sections together, you have the answer how the softwareSystem, person, and systemContextView methods work. These methods simply take a single Closure parameter and, by omitting the parenthesis, we get a nice Config-ish syntax:
void take(Closure c) {
c()
}
take {
println "I'm a closure!"
}
// prints: I'm a closure!
Closure Delegation
The last piece required to create a Groovy DSL like the one above is closure delegation. By default, you are allowed to use in a closure all the methods that are available in its outer scope (AKA the owner).
void take(Closure c) {
c()
}
void talk() {
println("I'm a closure!")
}
take {
talk()
}
// prints: I'm a closure!
For DSL purposes, the Closure class has a delegate parameter that allows us to extend the pool of available methods with methods of an arbitrary object. An example might do a better job explaining this than words:
class Talker {
void talk() {
println("I'm a closure!")
}
}
void take(Closure c) {
c.delegate = new Talker()
c()
}
take {
talk()
}
// prints: I'm a closure!
If we were to convert the take method to an English conversation, it basically tells the closure: “Hey, if you can’t find a required method, try delegating to this Talker object.” And so it does delegate in our example.
Now, this is enough to get some basic delegation working, but the IDEs will get lost without a bit of help. How is the IDE supposed to know what kind of object are you going to delegate to, huh? Well, there’s an annotation for that.
class Talker {
void talk() {
println("I'm a closure!")
}
}
void take(@DelegatesTo(Talker) Closure c) {
c.delegate = new Talker()
c()
}
take {
talk()
}
// prints: I'm a closure!
With this annotation in place, the IDE correctly suggests using Talker’s methods inside the closure. At this point, it should be clear where the methods like person, name, description etc. are taken in subsequent closures of our HelloWorld example.
Now, I’d love to say that it’s enough to start writing the DSL itself, but there’s one more little thing. By default, the closure first looks for methods inside its outer scope and then asks the delegation object. This default behavior would be bad for our softwareSystem method, which has two different meanings in two different scopes.
void softwareSystem() {
println "BUHAHAHA!"
}
class ViewConfiguration {
void softwareSystem() {
println "Expected behavior."
}
}
void take(@DelegatesTo(ViewConfiguration) Closure c) {
c.delegate = new ViewConfiguration()
c()
}
take {
softwareSystem()
}
// prints: BUHAHAHA!
Luckily, Groovy allows us to change this default behavior. We simply need to change the Closure’s resolveStrategy.
void softwareSystem() {
println "BUHAHAHA!"
}
class ViewConfiguration {
void softwareSystem() {
println "Expected behavior."
}
}
void take(@DelegatesTo(value = ViewConfiguration, strategy = DELEGATE_FIRST) Closure c) {
c.delegate = new ViewConfiguration()
c.resolveStrategy = DELEGATE_FIRST
c()
}
take {
softwareSystem()
}
// prints: Expected behavior.
Huh, that’s it! We’re ready to create an actual, useful DSL.
Groovy DSL for Structurizr
I trust that you’re a smart person and that I did a good job explaining in the previous section, so we’ll rush through the DSL implementation, rather than walking it step by step.
The entry point to our DSL is the Structurizr class. It has only one method, which initializes configuring a workspace. It is the same method that we used in the first line of our HelloWorld example’s main.
class Structurizr {
static Workspace workspace(
@DelegatesTo(value = WorkspaceConfigurer, strategy = DELEGATE_FIRST) Closure configurer) {
def wc = new WorkspaceConfigurer()
configurer.delegate = wc
configurer.resolveStrategy = DELEGATE_FIRST
configurer()
return wc.apply()
}
}
As you can see, we’re just delegating to a WorkspaceConfigurer here and expecting it to return a complete Workspace object afterward.
This “configurer” is just one of many similar classes, each responsible for configuring a single thing in the workspace. Each of them keeps necessary data and references to other related “configurers.” Our WorkspaceConfigurer currently looks like this:
class WorkspaceConfigurer {
String name
String description
List<SoftwareSystemConfigurer> softwareSystems = []
List<PersonConfigurer> people = []
List<SystemContextViewConfigurer> systemContextViews = []
Workspace apply() {
def workspace = new Workspace(name, description)
applyConfigurers(workspace)
return workspace
}
void applyConfigurers(Workspace workspace) {
softwareSystems.forEach { it.apply(workspace) }
people.forEach { it.apply(workspace) }
systemContextViews.each { it.apply(workspace) }
}
void name(String name) {
this.name = name
}
void description(String description) {
this.description = description
}
void softwareSystem(@DelegatesTo(value = SoftwareSystemConfigurer, strategy = DELEGATE_FIRST) Closure configurer) {
def ssc = new SoftwareSystemConfigurer()
configurer.delegate = ssc
configurer.resolveStrategy = DELEGATE_FIRST
configurer()
softwareSystems.add(ssc)
}
void person(@DelegatesTo(value = PersonConfigurer, strategy = DELEGATE_FIRST) Closure configurer) {
def pc = new PersonConfigurer()
configurer.delegate = pc
configurer.resolveStrategy = DELEGATE_FIRST
configurer()
people.add(pc)
}
void systemContextView(
@DelegatesTo(value = SystemContextViewConfigurer, strategy = DELEGATE_FIRST) Closure configurer) {
def scvc = new SystemContextViewConfigurer()
configurer.delegate = scvc
configurer.resolveStrategy = DELEGATE_FIRST
configurer()
systemContextViews.add(scvc)
}
}
This piece of code might seem complicated at first, but actually, it’s really simple. We’ve got two “real” fields in the class: name and description. For these fields, we expose methods with the same names. For each of the elements that will be configured in a separate closure, we need to expose a delegating method and remember the configurer that we’re delegating towards. After all the configuration is done and apply is called, we simply set the name and description, and let the other configurers do their job.
Now, we could go through each of the configurers one by one and explain what they do, but there’s no point. They are really similar and we’d be wasting your precious time. If you want to see a more complete version of the DSL’s codebase, you can find it here.
Summary
Creating a configuration DSL in Groovy is a piece of cake. Once you grasp the concepts of Closures and delegation, all you need to do is simply bash the necessary methods through some helper (configurer) objects. As you can see, although there is some typing involved, you can easily create a nice DSL for the tools you like if they lack a friendly API. Mine was Structurizr. What’s yours?
Published at DZone with permission of Grzegorz Ziemoński, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments