Software Development

Cloud Foundry Application manifest using Kotlin DSL

I had a blast working with and getting my head around the excellent support for creating DSL’s in Kotlin Language.

This feature is now being used for creating gradle build filesfor defining routes in Spring Webflux, for creating html templates using kotlinx.html library.

Here I am going to demonstrate creating a kotlin based DSL to represent a Cloud Foundry Application Manifest content.

A sample manifest looks like this when represented as a yaml file:

applications:
 - name: myapp
   memory: 512M
   instances: 1
   path: target/someapp.jar
   routes:
     - somehost.com
     - antother.com/path
   envs:
    ENV_NAME1: VALUE1
    ENV_NAME2: VALUE2

And here is the kind of DSL I am aiming for:

cf {
    name = "myapp"
    memory = 512(M)
    instances = 1
    path = "target/someapp.jar"
    routes {
        +"somehost.com"
        +"another.com/path"
    }
    envs {
        env["ENV_NAME1"] = "VALUE1"
        env["ENV_NAME2"] = "VALUE2"
    }
}

Getting the basic structure

Let me start with a simpler structure that looks like this:

cf {
    name = "myapp"
    instances = 1
    path = "target/someapp.jar"
}

and want this kind of a DSL to map to a structure which looks like this:

data class CfManifest(
        var name: String = "",
        var instances: Int? = 0,
        var path: String? = null
)

It would translate to a Kotlin function which takes a Lambda expression:

fun cf(init: CfManifest.() -> Unit) {
 ...
}

The parameter which looks like this:

() -> Unit

is fairly self-explanatory, a lambda expression which does not take any parameters and does not return anything.

The part that took a while to seep into my mind is this modified lambda expression, referred to as a Lambda expression with receiver:

CfManifest.() -> Unit

It does two things the way I have understood it:

1. It defines in the scope of the wrapped function an extension function for the receiver type – in my case the
CfManifest class

2. this within the lambda expression now refers to the receiver function.

Given this, thecf function translates to :

fun cf(init: CfManifest.() -> Unit): CfManifest {
    val manifest = CfManifest()
    manifest.init()
    return manifest
}

which can be succinctly expressed as:

fun cf(init: CfManifest.() -> Unit) = CfManifest().apply(init)

so now when I call:

cf {
    name = "myapp"
    instances = 1
    path = "target/someapp.jar"
}

It translates to:

CFManifest().apply {
  this.name = "myapp"
  this.instances = 1
  this.path = "target/someapp.jar"
}

More DSL

Expanding on the basic structure:

cf {
    name = "myapp"
    memory = 512(M)
    instances = 1
    path = "target/someapp.jar"
    routes {
        +"somehost.com"
        +"another.com/path"
    }
    envs {
        env["ENV_NAME1"] = "VALUE1"
        env["ENV_NAME2"] = "VALUE2"
    }
}

The routes and the envs in turn become methods on theCfManifest class and look like this:

data class CfManifest(
        var name: String = "",
        var path: String? = null,
        var memory: MEM? = null,
        ...
        var routes: ROUTES? = null,
        var envs: ENVS = ENVS()
) {

    fun envs(block: ENVS.() -> Unit) {
        this.envs = ENVS().apply(block)
    }

    ...

    fun routes(block: ROUTES.() -> Unit) {
        this.routes = ROUTES().apply(block)
    }
}

data class ENVS(
        var env: MutableMap<String, String> = mutableMapOf()
)

data class ROUTES(
        private val routes: MutableList<String> = mutableListOf()
) {
    operator fun String.unaryPlus() {
        routes.add(this)
    }
}

See how theroutes method takes in a Lambda expression with a receiver type ofROUTES, this allows me to define an expression like this:

cf {
    ...
    routes {
        +"somehost.com"
        +"another.com/path"
    }
    ...
}

Another trick here is way a route is being added is using :

+"somehost.com"

which is enabled using a Kotlin convention which translates specific method names to operators, here the unaryPlus method. The cool thing for me is that this operator is visible only in the scope of ROUTES instance!

Another feature of the DSL making use of Kotlin features is the way a memory is specified, there are two parts to it – a number and the modifier, 2G, 500M etc.

This is being specified in a slightly modified way via the DSL as 2(G) and 500(M).

The way it is implemented is using another Kotlin convention where if a class has aninvoke method then instances can call it the following way:

class ClassWithInvoke() {
    operator fun invoke(n: Int): String = "" + n
}
val c = ClassWithInvoke()
c(10)

So implementing invoke method as an extension function onInt in the scope of theCFManifest class allows this kind of a DSL:

data class CfManifest(
        var name: String = "",
        ...
) {
    ...
    operator fun Int.invoke(m: MemModifier): MEM = MEM(this, m)
}

This is pure experimentation on my part, I am both new to Kotlin as well as Kotlin DSL’s so very likely there are a lot of things that can be improved in this implementation, any feedback and suggestions are welcome. You can play with this sample code at my github repo here

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button