The power of Kotlin’s DSL and script engine

This is the article of LINE Advent Calendar 2018. My name is Freddie Wang, I am a software engineer at LINE KYOTO office. In this article, I will demonstrate how to use the Kotlin scripts + DSL to make flexible system and use a simple Clova Extension project as the example.

DSL (Domain Specific Language)

First of all, what is DSL? Let’s take a look at Wikipedia’s definition on DSL.

domain-specific language (DSL) is a computer language specialized to a particular application domain. This is in contrast to a general-purpose language (GPL), which is broadly applicable across domains.

So a DSL is a language which focuses on the particular part of the application. For example, SQL is one of the well-known DSL. We can use SQL to flexibly manipulate data.

DSL with Kotlin

Function literals with receiver is a cool feature of Kotlin, which provides the ability to call an instance of function type with receiver providing the receive object. With this feature, we can create Kotlin-based DSL very easily. For example, The Clova CEK SDK for Kotlin already supports simple DSL like this

clovaClient("application.id") {   
    launchHandler { launchRequest, session ->
        //Add your implementation
    }

    intentHandler { intentRequest, session ->
        //Add your implementation
    }

    sessionEndedHandler { sessionEndedRequest, session ->
        //Add your implementation
    }
}

You can use this Clova Extension DSL to create a Clova Extension very easily.

The Java Scripting API (JSR-223)

The Java Scripting API is a tool for using scripting engines from Java code (such as Nashorn). It enables users to write customizable scripting code that can be picked up by the Java application at runtime. After Kotlin 1.1, JSR-223 is supported for Kotlin Scripts too. That means that it’s possible to run the Kotlin scripts from Kotlin programs. The best thing is that it also supports to run the DSL from Kotlin programs!

How to create the custom Kotlin DSL

Creating the custom Kotlin DSL is very very simple, it is just like to create a function and use function literals with receiveras final parameter. For example, if we want to define clovaClient("application.id"), we can declare a function like this

fun clovaClient(applicationId: String,
    verifier: RequestVerifier = RequestVerifierImpl(),
    init: ClovaClient.() -> Unit
): ClovaClient {
    val client = ClovaClient(applicationId, verifier)
    client.init()
    return client
}

Here the init would become a function of ClovaClient. The body of init would be defined by the DSL users.

Run the DSL via the Kotlin script engine

To enable the Kotlin script engine, we need to include the dependencies first. There are 3 necessary packages which should be included in the build.gradle.

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-script-util:$kotlinVersion"
    implementation "org.jetbrains.kotlin:kotlin-script-runtime:$kotlinVersion"
    implementation "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlinVersion"
}    

Then a file called javax.script.ScriptEngineFactory has to be placed in the folder META-INF/services . It contains this entry org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory. Or alternatively you can define a custom scripting engine factory. After that you can initilize the script engine like this

val scriptEngine: ScriptEngine = ScriptEngineManager().getEngineByExtension("kts")!!

Once the script engine is initilized, we should be able to run the Clova DSL as below.

val clovaClient = scriptEngine.eval(dsl) as ClovaClient

Very simple, isn’t it?

Running with Spring Boot

If you are using Spring Boot, the Spring Boot gradle plugin would pack all dependencies into single executable jar. It is call fat jar. In most cases it can work well. However certain libraries may have problem and kotlin-compiler-embeddable is one of them. To deal with the problematic libraries, the executable jar should be configurated to unpack specific jar files. So we need to add configurations like this.

bootJar {
   requiresUnpack "**/kotlin*.jar", "**/clova-extension-*.jar"
}

Unfortunatelly, you can’t just run the DSL directly like above in Spring Boot. This is because the classpath in the Kotlin script engine would be different from the application. So you need to add the import packages before the DSL. The full code snippets would look like this

import com.linecorp.clova.extension.client.*
import com.linecorp.clova.extension.model.util.*
import com.linecorp.clova.extension.converter.jackson.JacksonObjectMapper

clovaClient("application.id") {
    objectMapper = JacksonObjectMapper()

    launchHandler { launchRequest, session ->
        //Add your implementation
    }

    intentHandler { intentRequest, session ->
        //Add your implementation
    }

    sessionEndedHandler { sessionEndedRequest, session ->
        //Add your implementation
    }
}

Now, we can run the Kotlin DSL inside the application. Even if we can update the DSL from above in an external editor, we can change the behavior of Clova Extension without re-deploying the application again!

I’ve created a simple online Clova Extension editor by using Kotlin DSL, Kotlin script engine and The Clova CEK SDK for Kotlin. You can use this online editor to create the Clova Extension directly. the source code is here

Conclusion

Now Kotlin become more and more popular in the world. Relatively, Kotlin script engine is not famous as Kotlin language. By the power of Kotlin DSL and Kotlin script engine, you can create a flexible system very easily. If you have any question or idea, please feel free to contact with me @wangyung on Twitter. I am looking forward to see any exciting idea created by Kotlin.

Tomorrow (12/16) is Jun’s Tensorflow.jsの話, If you’are interested in machine learning, please don’t miss it!

Related Post