zsmb13
zsmb13

Reputation: 89668

Kotlin type safe builder DSLs, safety for the outermost function

I'm going to use the official example from the documentation that implements a DSL for some HTML creation.

Since Kotlin 1.1, the @DslMarker annotation allows us to restrict the scope of the functions in our classes, like the example does with the @HtmlTagMarker annotation. This gives us an error when trying to write incorrectly structured code like this:

html {
    body { 
        body { // this in an error, as it's a function call on the outside Html element
        }
    }
}

However, this doesn't prevent nesting the outermost function, which is the entry point to the DSL. For example, with the example as it is now, this can be written down without problems:

html {
    html {
    }
}

Is there any way to make a DSL safer in this regard?

Upvotes: 10

Views: 490

Answers (1)

hotkey
hotkey

Reputation: 148159

Probably this can somehow be done in a more elegant way, but I can suggest using the @Deprecated annotation with DeprecationLevel.ERROR on a function with a matching signature defined for the receiver type, for example:

@Deprecated("Cannot be used in a html block.", level = DeprecationLevel.ERROR)
fun HtmlReceiver.html(action: HtmlReceiver.() -> Unit): Nothing = error("...")

Or this can be a member function. By the way, the IDE completion behaves a bit differently based on whether it is an extension or a member.

This will make the calls like the inner one invalid:

html {
    html { // Error: Cannot be used in a html block.
    }
}

(demo of this code)

The top-level function can still be called inside a DSL block by its FQN e.g. com.example.html { }, so this trick only protects the users from calling the top level function by mistake.

Upvotes: 11

Related Questions