Creating a DSL

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.163.62