You have seen Jenkins pipelines and Gradle build configs written in Groovy, ever wondered how they are made of? This two-part series is about creating your own custom Groovy based DSLs.
DSLs or “Domain Specific Languages” are specialized languages for specific purposes such as configuring a server, declaring CI pipelines and build scripts. DSLs have existed since long, but most have been implemented by using a full-blown parser and an expression evaluator evaluator. But with declarative friendly languages like Groovy and Kotlin, we can easily develop a DSL for our needs by simply creating a bunch of classes and static methods.
Groovy is a dynamic object-oriented programming language for the Java virtual machine (JVM) that can be used anywhere Java is used. It is widely used to extend existing Java applications and write scripts and configurations. Groovy based DSLs are used at many places — Jenkins pipelines and Gradle build configuration are the few of the well-known examples. Not only configuration scripts, DSLs are also used as entry-point for various APIs owing to their succinct syntax (such as the Kafka Streams library).
DSLs in Groovy heavily utilize a language feature called Closure. A Closure in Groovy is an open, anonymous, block of code that can take arguments, return a value and be assigned to a variable. Essentially, the DSLs consist of several closures with method calls which are applied over Builder classes to yield configuration objects.
Let’s take an example for the configuration of a log collection agent (think Logstash). We will be building a log collection pipeline with input, mappers, filters and output. Our goal is to have a DSL like:
Every “keyword” in a Groovy DSL is a method call, so we start with the pipeline function. The function accepts a Closure, set’s its delegate to the newly created PipelineSpec object and invokes it. This results in all method calls inside the Closure invoked as if they have been invoked on the
PipelineSpec class is a simple Builder class.
Using interfaces and Closures, we can easily extend out DSL to include custom inline logic such as the user-defined filter in the above example.
Let’s take another example of a CI pipeline (inspired from Jenkins). We want our DSL to look like:
This uses several nested Closures, which will be reflected in our Spec classes.
DSLs like this can be used as programming APIs as part of compiled code as well as configuration scripts by loading them from the disk and evaluating them at the run-time.
In the next post I shall cover run-time loading of configuration scripts (like how Gradle loads
build.gradle) and import customization for even more beautiful DSLs.