Domain-Specific Language is a mini language for a specific problem and/or in a narrow context. For example, internally used automation tools usually define some small DSL for configuration and most users understand the context and what DSL offers.
This blog post offers my simplistic view of how an internal DSL is implemented in Groovy via closure delegation. It shows the progression from standard Java-like implementation -> its fluent version -> final DSL form. This might help undrestanding the inner workings of a DSL such as Jenkins’s Pipeline steps. There are probably more advanced methods/frameworks for creating DSL. However, those are not in the scope of this post.
Example DSL
We want to implement a simple DSL that is similar to Pipeline steps in Jenkinsfile.
1 2 3 4 5 6 |
|
In this DSL example, users will write a sequence of steps using a small, pre-defined set of custom statements such as echo
and sh
above.
For each step in the DSL, the backend classes and objects will perform some execution in the background, using the relevant context specific to the domain (e.g., Jenkins domain).
For simplicity, println
statements will be used in the following examples.
The advantage of DSL is that the developers can implement the backend in some fully-featured language such as Java but the users don’t need to know such language to use it. Such a separation is common in DevOps and automation frameworks where the users want the flexibility of configuring based on their needs but don’t want to get exposed to the implementation details (which are usually ugly and compplicated). Instead, the users only need to learn the DSL to use it while still have the flexibility to do what they want. One example can be found in data science domain where data scientists are usually more comfortable developing in R or SQL but automated deployment frameworks or tools can be in another language such as Java.
Version 1: Java-like standard implementation
First, we show a standard implementation in Java to show how backend execution can be implemented. In the advanced versions, the difference is only in its public interface to make it more user-friendly but the backend execution will be similar.
1 2 3 4 5 6 7 8 9 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 35 36 37 |
|
The problem of this approach is that users have to write Java (or Groovy) code directly to use it.
Version 2: Fluent interface with Builder pattern
1 2 3 4 5 6 7 8 9 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 35 36 37 38 39 40 |
|
In this version, the Build design pattern is used in the implementation.
As shown above, the code is much more fluent with the object name builderDsl
not being repeated every single line.
As a result, the code is less verbose and much more user-friendly.
Version 3: DSL with Groovy closure
1 2 3 4 5 6 7 8 9 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 35 36 37 38 39 |
|
This first version of Groovy implementation is presented here to show connection with its Java counterparts.
As shown below, the input variable dsl
in the closure can be abstracted away using delegate.
1 2 3 4 5 6 7 8 9 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
In this final version, only a very small boiler-plate code GroovyDsl.executeBest
remains.
The following lines form a mini language (i.e., DSL) that can be exposed to users.
The users can start using the DSL without having to learn Groovy or Java.
Note that the executeBest
is the equivalent but less straight-forward way to do the same thing with delegate.
Compared with execute
, it has the benefit of NOT modifying the input reference closure
.