kotlindslkotlin-dsl

Kotlin DSL with optional fields


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!


Solution

  • 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.