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?
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With