One of my big passions is cycling. The emotion of movement, the effort, the health benefits, and enjoying the landscape are some of the benefits (and I can keep going on and on).
I want to create a way to have a registry of my bikes and their components. For the prototype phase, I'll use XML, but later on we can change to a different implementation:
<bicycle description="Fast carbon commuter">
<bar material="ALUMINIUM" type="FLAT">
</bar>
<frame material="CARBON">
<wheel brake="DISK" material="ALUMINIUM">
</wheel>
</frame>
<fork material="CARBON">
<wheel brake="DISK" material="ALUMINIUM">
</wheel>
</fork>
</bicycle>
This is the perfect scenario to create a type-safe builder in Kotlin.
In the end, my bicycle DSL should look like this:
fun main(args: Array<String>) {
val commuter = bicycle {
description("Fast carbon commuter")
bar {
barType = FLAT
material = ALUMINIUM
}
frame {
material = CARBON
backWheel {
material = ALUMINIUM
brake = DISK
}
}
fork {
material = CARBON
frontWheel {
material = ALUMINIUM
brake = DISK
}
}
}
println(commuter)
}
My DSL is regular Kotlin code, is compiled fast, and my IDE will help me to autocomplete, and will complain when I make a mistakeāa win-win situation.
Let's start with the program:
interface Element {
fun render(builder: StringBuilder, indent: String)
}
All parts of my bicycle in my DSL will extend/implement the Element interface:
@DslMarker
annotation class ElementMarker
@ElementMarker
abstract class Part(private val name: String) : Element {
private val children = arrayListOf<Element>()
protected val attributes = hashMapOf<String, String>()
protected fun <T : Element> initElement(element: T, init: T.() -> Unit): T {
element.init()
children.add(element)
return element
}
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent<$name${renderAttributes()}> ")
children.forEach { c -> c.render(builder, indent + " ") }
builder.append("$indent</$name> ")
}
private fun renderAttributes(): String = buildString {
attributes.forEach { attr, value -> append(" $attr="$value"") }
}
override fun toString(): String = buildString {
render(this, "")
}
}
Part is the base class for all my parts; it has children and attributes properties; it also inherits the Element interface with an XML implementation. Changing to a different format (JSON, YAML, and others) should not be too difficult.
The initElement function receives two parameters, an element T and an init function with receiver T.() -> Unit. Internally, the init function is executed and the element is added as children.
Part is annotated with an @ElementMarker annotation, that is itself annotated with @DslMarker. It prevents inner elements from reaching outer elements.
In this example, we can use frame:
val commuter = bicycle {
description("Fast carbon commuter")
bar {
barType = FLAT
material = ALUMINIUM
frame { } //compilation error
}
It is still possible to do it explicitly with this qualified:
val commuter = bicycle {
description("Fast carbon commuter")
bar {
barType = FLAT
material = ALUMINIUM
this@bicycle.frame{ }
}
Now, several enumerations to describe materials, bar types, and brakes:
enum class Material {
CARBON, STEEL, TITANIUM, ALUMINIUM
}
enum class BarType {
DROP, FLAT, TT, BULLHORN
}
enum class Brake {
RIM, DISK
}
Some of these parts have a material attribute:
abstract class PartWithMaterial(name: String) : Part(name) {
var material: Material
get() = Material.valueOf(attributes["material"]!!)
set(value) {
attributes["material"] = value.name
}
}
We use a material property of type Material enumeration, and we store it inside the attributes map, transforming the value back and forth:
class Bicycle : Part("bicycle") {
fun description(description: String) {
attributes["description"] = description
}
fun frame(init: Frame.() -> Unit) = initElement(Frame(), init)
fun fork(init: Fork.() -> Unit) = initElement(Fork(), init)
fun bar(init: Bar.() -> Unit) = initElement(Bar(), init)
}
Bicycle defines a description function and functions for frame, fork, and bar. Each function receives an init function that we pass directly to initElement.
Frame has a function for the back wheel:
class Frame : PartWithMaterial("frame") {
fun backWheel(init: Wheel.() -> Unit) = initElement(Wheel(), init)
}
Wheel has a property brake using the Brake enumeration:
class Wheel : PartWithMaterial("wheel") {
var brake: Brake
get() = Brake.valueOf(attributes["brake"]!!)
set(value) {
attributes["brake"] = value.name
}
}
Bar has a property for its type, using the BarType enumeration:
class Bar : PartWithMaterial("bar") {
var barType: BarType
get() = BarType.valueOf(attributes["type"]!!)
set(value) {
attributes["type"] = value.name
}
}
Fork defines a function for the front wheel:
class Fork : PartWithMaterial("fork") {
fun frontWheel(init: Wheel.() -> Unit) = initElement(Wheel(), init)
}
We are close to the finish, the only thing that we need now is an entry function for our DSL:
fun bicycle(init: Bicycle.() -> Unit): Bicycle {
val cycle = Bicycle()
cycle.init()
return cycle
}
And that's all. DSLs in Kotlin with the infix functions, operator overloading, and type-safe builders are extremely powerful, and the Kotlin community is creating new and exciting libraries every day.