I'm currently in the process of learning about Kotlin DSLs.
I've been playing around with it for a while now but I'm unable to solve my use case. I have a simple DSL, and I don't care too much of the types it has as long as I can achieve a syntax like this:
private fun getObj(): SET {
return SET {
ITEM {
A = null
B = "Hello world"
C
// D - exists in DSL but omitted here
}
}
}
In the background, I now want to distinguish between certain values set in the ITEM
block. B
is easy, it is simply the value, but it becomes hard for A
and C
. Somehow I'm not able to differentiate between null
and no value
set. Currently my builder looks like this, but I'm open to changing it to achieve the syntax above:
class ITEMBuilder {
var A: String? = null
var B: String? = null
var C: String? = null
var D: String? = null
fun build() = ITEM(
ItemValue(A),
ItemValue(B),
ItemValue(C),
ItemValue(D)
)
}
class ItemValue(val include: Boolean? = false, val value: String? = null) {
constructor(value: String? = null): this(null != value, value)
}
When I have my final object, I want to be able to tell 4 different stages for each field under ITEM:
I tried different types, but had no luck since most things impact the syntax. I also tried to change the getter/setters in the builder, to maybe catch the update there and have an additional internal property that gets updated - but neither get
or set
are called for null/no value. Also tried to change the fields to functions but then I have ugly parenthesis ()
in the DSL syntax.
Would be great if somebody could help me figure this out.
Thanks in advance!
You can use receivers to achieve that. Here's an example with a single parameter (field in this case)
//Use like this
val item = ITEM {
A = "Yay"
B //Not omitted, but also not set
//C can be omitted
D = null
}
This is equivalent to
Item(//Included and set to "Yay"
a=ItemValue(include=true, hasBeenSet=true, value="Yay"),
//Included, but not yet set
b=ItemValue(include=true, hasBeenSet=false, value=null),
//Not included, and so not yet set
c=ItemValue(include=false, hasBeenSet=false, value=null),
//Included, and set to null (Same as A)
d=ItemValue(include=true, hasBeenSet=true, value=null))
You can do this with the help of extra fields of type String?
and override their setters to modify the actual fields of type ItemValue
. I included a hasBeenSet
property in ItemValue
to show whether or not it has been set.
To mark properties as included without setting them, you can override the getters so they modify the actual fields to make them ItemValue(true, false)
, which means they're included but not set.
class Builder {
var A: String? = null
set(value) {
a = ItemValue(true, true, value)
}
get() {
a = ItemValue(true, false)
return field
}
var B: String? = null
set(value) {
b = ItemValue(true, true, value)
}
get() {
b = ItemValue(true, false)
return field
}
var C: String? = null
set(value) {
c = ItemValue(true, true, value)
}
get() {
c = ItemValue(true, false)
return field
}
var D: String? = null
set(value) {
d = ItemValue(true, true, value)
}
get() {
d = ItemValue(true, false)
return field
}
var a: ItemValue = ItemValue(false, false)
var b: ItemValue = ItemValue(false, false)
var c: ItemValue = ItemValue(false, false)
var d: ItemValue = ItemValue(false, false)
fun build(): Item {
return Item(a, b, c, d)
}
}
fun ITEM(setters: Builder.() -> Unit): Item {
val builder = Builder()
builder.setters()
return builder.build()
}
data class Item(val a: ItemValue, val b: ItemValue, val c: ItemValue, val d: ItemValue)
data class ItemValue(val include: Boolean, val hasBeenSet: Boolean, val value: String? = null)
Here's the link to the Kotlin Playground. You can run it and see the output for yourself.