Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Cloud Foundry Application Manifest Using a Kotlin DSL

DZone's Guide to

Cloud Foundry Application Manifest Using a Kotlin DSL

Kotlin is a popular language choice for creating DSLs. Here, we use its functional capabilities, i.e. lambda expressions, to represent a Cloud Foundry App manifest.

· Java Zone
Free Resource

Just released, a free O’Reilly book on Reactive Microsystems: The Evolution of Microservices at Scale. Brought to you in partnership with Lightbend.

I had a blast working with and getting my head around the excellent support for creating DSLs in Kotlin Language. This feature is now being used for creating Gradle build files, for defining routes in Spring Webflux, and for creating HTML templates using kotlinx.html library.

Here, I am going to demonstrate how to create 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 I want this kind of DSL to map to a structure that 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 looks like this:

() -> Unit


That is fairly self-explanatory: a lambda expression that 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 a receiver:

CfManifest.() -> Unit


It does two things, at least 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, the cf 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 the CfManifest 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 the routes method takes in a lambda expression with a receiver type of ROUTES? This allows me to define an expression like this:

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


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

+"somehost.com"


Which is enabled using a Kotlin convention that 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 an invoke 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 the invoke method as an extension function on Int in the scope of the CFManifest 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 DSLs so it's very likely that 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.

Strategies and techniques for building scalable and resilient microservices to refactor a monolithic application step-by-step, a free O'Reilly book. Brought to you in partnership with Lightbend.

Topics:
cloud foundry ,kotlin dsl ,java ,manifest ,tutorial

Published at DZone with permission of Biju Kunjummen, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}