1

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:

  • value set
  • null set
  • no value set
  • field omitted

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!

apangin
  • 92,924
  • 10
  • 193
  • 247
Stefan
  • 214
  • 1
  • 12

1 Answers1

1

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.

user
  • 7,435
  • 3
  • 14
  • 44
  • This looks good and is solving for the omitted case. However it is not solving for `B` and `C`, a null value and no value. The desired outcome would be that the ItemValue class shows `include=true` for all but the omitted case. Thinking more about it, I would most definitely have to expand the `ItemValue` class, the desired object outcome would look something like this: `-> isIncludes, Value, isEmpty` and then the use cases would look like: `A: ItemValue == (true, A, false)` `B: ItemValue == (true, B, false)` `C: ItemValue == (true, C, true)` `D: ItemValue == (false, D, true)` – Stefan May 05 '20 at 19:31
  • Your updated code covers the case for null values, however it is not able to distinguish between omitted values and values that are not set, ```A = null // works```, ```B = "Hello world" // works```, ```C // doesnt work```, ```// D - exists in DSL but omitted here // works``` – Stefan May 05 '20 at 20:34
  • Yep, that solved it. Thanks for all the help. Let me read through the code, to understand how this works. – Stefan May 05 '20 at 21:22