Kotlin DSLs can be used to replace a boring test data strings with correct and an easy to read a code.
That time I was working on java9c plugin for Gradle, I created integration tests. In my case all those tests were of the following pattern: create sample Gradle project, execute it, check results. I decided to run a fun experiment and replace boring string constants with a Gradle-looking DSL.
Namely, instead of (and many Gradle plugin integration tests have similar)
I created a tiny DSL that looks (and parses) like a Gradle script. The DSL generates test-data files for me. Kotlin compiler and IDE helps to prevent errors before a test is executed. Code completion makes a new test authoring easier. Here is the DSL usage example:
More examples are on GitHub in
java9c plugin test sources.
The example looks like a Gradle script. There was no goal to make it 100% same looking. Also, there is an amazing project run by Gradle to support Kotlin DSLs in Gradle, natively. That DSL is not 100% same looking to a Gradle-Groovy scripts too.
The implementation of that my magic DSL fits in one file, and it’s about 150 lines.
Next, I’ll explain how one can create similar DSLs for their own needs. With Kotlin you may target JVM, Android, JS, and Native, reusing same pure-Kotlin code.
We need to create a text generator DSL. The primary decision if either to generate a bare text or to use an API of a library. You may consider XML DOM or Jackson. There are kotlinpoet or javapoet to generate Kotlin or Java code via an API. I bet there are many other libraries. Nebula Gradle Lint Plugin can read/write Gradle scripts too.
There is a trade-off. Dealing with a library may be complicated and time-consuming, but way more stable.
For the sake of java9c tests, I decided to implement the generator based on bare text output. And it’d be me who covers all risks and bugs from the implementation. It is only about 150 lines (now) of code.
I started with a line writer interface:
Inside the interface, I use unary minus, e.g.,
-"foo" as the function to write a line. It reads better in DSLs, e.g.
Well, you may decide to have a
fun line(text: String) instead. That does not change the rest, so, please
feel free to use a function instead of an operator. Alternatively, you may use
String.unaryPlus, so that
See Operator Overloading documentation or ask me, if you need to clarify the trick.
A Trivial Writer Implementation
The implementation of the interface could be something trivial, e.g.
The usage could be:
generateDSL receives a lambda with receiver
and returns resulting string. It is the implementation detail to pass the instance of
LineWriter to the lambda. Inside the lambda, the receiver is
LineWriter, it means, that
resolves to an instance of
LineWriter. Of course,
this. can be omitted and all methods are resolved
LineWriter instance. It follows that
- "foo" calls resolves to
LineWriter inside the lambda scope.
For short, we may compact the
generateDSL function to the following:
Here I use a
fun buildString(builderAction: StringBuilder.() -> Unit): String function from the Kotlin standard library.
It receives yet another lambda with receiver
StringBuilder type so that
appendln is a function from it.
Theoretically, you may have several different entry point functions (e.g.,
to, say, generate a string, a file or something else. The rest does not depend on a particular
Indenting and Blocks
Text generation for languages like Gradle requires indenting. We have blocks, and it’s nice
to simplify blocks generation. At first, I created an
offset function for it:
The function is an extension function. It makes
no need to change the original
LineWriter interface, but it still reads as a method call.
block function is as follows:
Here I use string interpolation to simplify code of the first line.
At that point I can write the DSL snippets like that:
And it yields
Nice, isn’t it?
Gradle Specific Constructs
block function one can create all necessary functions to generate blocks like
dependencies and so on.
Now it is time to implement specific parts of the DSL and allow some constructs only inside other constructs.
repositories block. Inside we have pre-defined functions for
First, we need a builder interface and implementation. It can be done as follows:
We define an interface
RepositoriesWriter to play as the scope of the generation. In the interface,
mavenLocal and other functions with trivial implementations. Those functions can be alternatively
implemented as extension functions of inside the
repositories function. That is up to the author.
repositories function, I use class delegation aka
keyword to implement
RepositoriesWriter to delegate to another instance of
So short to write and powerful!
As the result, we can have
The same way I created the whole bunch of functions to support the subset of Gradle scripts I was using in my tests. You may take a look here for more details.
Specific DSL Alternatives
It was another design decision to allow
LineWriter functions and extension functions (e.g.,
of the scope of
RepositoriesWriter lambda. We might have decided opposite. In the case, we would need
annotation to make sure
LineWriter functions and extension functions are not resolved to the
outer scope. We probably have a
generateDSL function call on the top.
DSLs are nice. In the post, I presented the DSL building pattern. Use it to create your DSLs. Ask me if you have questions. You may also check this article from Kotlin documentation or a video of a talk by Hadi.comments powered by Disqus