Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make field required in kotlin DSL builders

Tags:

kotlin

In Kotlin, when creating a custom DSL, what is the best way to force filling required fields inside the builder's extension functions in compile time. E.g.:

person {
    name = "John Doe" // this field needs to be set always, or compile error
    age = 25
}

One way to force it is to set value in a function parameter instead of the body of the extension function.

person(name = "John Doe") {
    age = 25
}

but that makes it a bit more unreadable if there are more required fields.

Is there any other way?

like image 875
redhead Avatar asked Dec 06 '18 12:12

redhead


2 Answers

New type inference enables you to make a null-safe compile-time checked builder:

data class Person(val name: String, val age: Int?)

// Create a sealed builder class with all the properties that have default values
sealed class PersonBuilder {
    var age: Int? = null // `null` can be a default value if the corresponding property of the data class is nullable

    // For each property without default value create an interface with this property
    interface Named {
        var name: String
    }

    // Create a single private subclass of the sealed class
    // Make this subclass implement all the interfaces corresponding to required properties
    private class Impl : PersonBuilder(), Named {
        override lateinit var name: String // implement required properties with `lateinit` keyword
    }

    companion object {
        // Create a companion object function that returns new instance of the builder
        operator fun invoke(): PersonBuilder = Impl()
    }
}

// For each required property create an extension setter
fun PersonBuilder.name(name: String) {
    contract {
        // In the setter contract specify that after setter invocation the builder can be smart-casted to the corresponding interface type
        returns() implies (this@name is PersonBuilder.Named)
    }
    // To set the property, you need to cast the builder to the type of the interface corresponding to the property
    // The cast is safe since the only subclass of `sealed class PersonBuilder` implements all such interfaces
    (this as PersonBuilder.Named).name = name
}

// Create an extension build function that can only be called on builders that can be smart-casted to all the interfaces corresponding to required properties
// If you forget to put any of these interface into where-clause compiler won't allow you to use corresponding property in the function body
fun <S> S.build(): Person where S : PersonBuilder, S : PersonBuilder.Named = Person(name, age)

Use case:

val builder = PersonBuilder() // creation of the builder via `invoke` operator looks like constructor call
builder.age = 25
// builder.build() // doesn't compile because of the receiver type mismatch (builder can't be smart-casted to `PersonBuilder.Named`)
builder.name("John Doe")
val john = builder.build() // compiles (builder is smart-casted to `PersonBuilder & PersonBuilder.Named`)

Now you can add a DSL function:

// Caller must call build() on the last line of the lambda
fun person(init: PersonBuilder.() -> Person) = PersonBuilder().init()

DSL use case:

person {
    name("John Doe") // will not compile without this line
    age = 25
    build()
}

Finally, on JetBrains open day 2019 it was said that the Kotlin team researched contracts and tried to implement contracts that will allow creating safe DSL with required fields. Here is a talk recording in Russian. This feature isn't even an experimental one, so maybe it will never be added to the language.

like image 187
Bananon Avatar answered Oct 16 '22 02:10

Bananon


In case you're developing for Android I wrote a lightweight linter to verify mandatory DSL attributes.

To solve your use case you will only need to add an annotation @DSLMandatory to your name property setter and the linter will catch any place when it is not assigned and display an error:

@set:DSLMandatory
var name: String

err

You can take a look here: https://github.com/hananrh/dslint/

like image 4
Hanan Rofe Haim Avatar answered Oct 16 '22 02:10

Hanan Rofe Haim