Auto-generate Kotlin DSL

Introduction
DSL (Domain Specific Language) is a pretty common way to provide an abstraction of an specific application domain. For example Kotlin, Java and Swift are “general-purpose languages”. SQL and HTML are “domain specific languages”.
Nowadays, Kotlin is getting more and more popular in the DSL world for the features it provides to create your own DSL:
- Higher-Order functions
- Lambdas with Receivers
- DslMarker
- Operator overloading
- and much more features that makes Kotlin the perfect language for this.
I’m not going to write much about how to use Kotlin to create your own DSL as there are plenty of great posts about this (also this and this talks from the KotlinConf 2018) but I would like to focus more in other aspects.
I faced some issues after trying to learn and develop my own DSL with Kotlin and I think maybe you will end up in the same situation. So I wanted to share some thoughts around this and a possible solution to alleviate it.
- Scary syntax?
- Immutability
- Boilerplate
- ✅ Tackle all at once
1. Scary syntax?
At first sight, the Kotlin syntax for creating a DSL could look a little scary. In the following code block we have one of the simplest DSL that you can create:
fun person(block: Person.() -> Unit): Person = Person().apply(block)class Person(var name: String? = null, var age: Int? = null)
This could be a little intimidating as we have plenty of concepts in just 2 lines of codes and this is just to being able to create a new Person
instance in this way:
val me = person {
name = "Juan"
age = 34
}
Just to give you some details on what it’s going on here:
- Higher-order functions to receive a lambda as parameter.
Person.() -> Unit
: Lambda with Receiver to be able to set later the name and age.()
means the function will not receive parameters andUnit
is the return type.apply
extension function +block
function being executed insidePerson
new instance.var name: String? = null
: Default and nullable values.- If last parameter is a function, then it can be defined outside parentheses
If you didn’t hear about one of these concepts then you can pick the links that I posted at the beginning of this post with different language features.
2. Immutability
I’m sure that you noticed that our Person
class has default and nullable parameters in the constructor. It means that you can create an instance of a person like this Person()
and name/age
are going to be null
. Well, this doesn’t look so good.
One approach to avoid this is to create a Builder:
fun person(block: PersonBuilder.() -> Unit): Person = PersonBuilder().apply(block).build()
class PersonBuilder() {
var name: String by Delegates.notNull()
var age: Int by Delegates.notNull()
fun build(): Person = Person(name, age)
}
This is much better! but now we have another class to maintain, so if we change Person
parameters then we need to update the Builder
.
3. Boilerplate
So every time we want to provide a DSL like the one we created for Person
class we need to create again another Builder, provide a function with all the fancy stuff that we reviewed and so on.
These are just some of the repeated tasks that brings creating your own DSL. After fighting with these and other tasks to maintain my DSLs, I decided to create something that helps me with all this process.
4. ✅ Tackle all at once
At the end, I found myself repeating these tasks again and again, and what is better to perform repeated task than a piece of software :) so here you have a library to help you creating DSLs.
AutoDsl
AutoDsl is a library that auto-generates Kotlin DSL using annotation processing.
This library will help you to create expressive, immutable and type-safe DSL without boilerplate.
In order to use it just add the repository and dependencies:
repositories {
maven { url "https://dl.bintray.com/juanchosaravia/autodsl" }
}dependencies {
api "com.juanchosaravia.autodsl:annotation:latest_version"
kapt "com.juanchosaravia.autodsl:processor:latest_version"
}
Note: Latest version is in the github repo.
Now let’s take the previous example and create the Person
DSL. Just define your class with the correct parameters (immutable) as you normally would do and add the @AutoDsl
annotation to the class like this:
@AutoDsl
class Person(val name: String, val age: Int)
And that’s it!
AutoDsl will generate the Builder
and the person
function for you:
person {
name = "Juan"
age = 34
}
AutoDsl Collections
Collections are one of the most exiting parts as AutoDsl
will take care of that for you and provide some useful functions. Let’s add some friends
to our Person
class:
@AutoDsl
class Person(val name: String,
val age: Int,
val friends: List<Person>?)
The created DSL allows you to add new friends like this:
person {
name = "Juan"
age = 34
friends {
+person {
name = "Arturo"
age = 28
}
+person {
name = "Tiwa"
age = 30
}
}
}
Out-of-the-box AutoDsl
will use the Kotlin feature “Operator overloading” to add friends with the +
keyword and then the Person DSL.
More features
There are more features that I invite you to review in more detail in the Wiki Page: https://github.com/juanchosaravia/autodsl/wiki
But here you have a brief summary of them:
- Custom DSL name: You can change the default DSL name:
@AutoDsl(dslName = "newPerson")
- Required and Optional parameters: Required parameters will be enforced by throwing an exception in case are not set. Optional ones are available by just adding
?
in the parameter definition. - Automatic DslMarker: Will provide DSL Marker context automatically for your annotated classes.
- Java Friendly: The generated Builder will have methods to be used from Java as a regular builder.
- AutoDslConstructor: Annotation to indicate which constructor use when you have more than one and it’s not the first one.
- AutoDslCollection: Annotation to give you more control over collections like specifying the concrete collection type or indicate the collection will not be nested but inlined.
AutoDsl is open source with Apache License v2.0 so feel free to contribute and report any bug in the github issue tracker.
I wanted to give a special thanks to Arturo Ayala 👏 🚀 who helps me in the development of the library. You can find him also in twitter.
If you liked this post please don’t forget to 👏 I’ll really appreciate it!
Twitter: https://twitter.com/juanchosaravia
Linkedin: https://www.linkedin.com/in/juansaravia
Cheers!