Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin type safe builder DSLs, safety for the outermost function

Tags:

kotlin

dsl

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?

like image 995
zsmb13 Avatar asked Apr 18 '17 09:04

zsmb13


People also ask

What is Kotlin type safe?

Type-safe builders allow creating Kotlin-based domain-specific languages (DSLs) suitable for building complex hierarchical data structures in a semi-declarative way. Sample use cases for the builders are: Generating markup with Kotlin code, such as HTML or XML. Configuring routes for a web server: Ktor.

What is DslMarker?

Classes that define annotations marked with the @DslMarker annotation are used to define DSLs. These annotations are used to mark classes and receivers, preventing receivers marked with the same annotation to be accessed inside one another.

How do I build DSL in Kotlin?

To make a DSL means to change the syntax of that specific part of the code. In Kotlin, this is achieved by using lambda and extension functions, and expressions, to remove a lot of boilerplate code and hide the internal implementation from the user.

What is Kotlinx HTML?

The kotlinx. html library provides the ability to generate DOM elements using statically typed HTML builders (and besides JavaScript, it is even available on the JVM target!) To use the library, include the corresponding repository and dependency to our build. gradle.


1 Answers

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.

like image 150
hotkey Avatar answered Nov 29 '22 10:11

hotkey