- Blog
- 08.14.2024
- Data Fundamentals
A Domain Specific Language (DSL) in Kotlin

In part one of this blog series, I described DSLs, their advantages, and the language features available in Kotlin that enable us to create an embedded Domain Specific Language in that programming language.
Part two continues the process, and I’ll show you how I used these features to create the beginnings of a DSL for testing OpenAPI services.
Project setup
For this project, I’ll be using Gradle as the build tool together with http4k, a lightweight HTTP library written in Kotlin. I’m also using Kotest to help with testing, as it too is Kotlin-native, with a rich library of assertion functions I can make use of in my tests.
The Gradle dependencies are:
dependencies {
implementation(platform("org.http4k:http4k-bom:5.24.0.0"))
implementation("org.http4k:http4k-core")
implementation("org.http4k:http4k-client-apache")
implementation("org.http4k:http4k-testing-kotest")
implementation("io.kotest:kotest-assertions-json:5.9.1")
}
Building the DSL
To create the DSL, I need a type-safe builder to initialize the HTTP request that the test will make. To do this, I first define a functional interface that can be extended to implement each different HTTP request that I will be testing:
import org.http4k.core.Request
fun interface ApiQuery {
fun createRequest(): Request
}
Creating a functional interface this way, as opposed to a normal interface, allows me to use lambdas to create an ApiQuery more easily if needed.
Using this in the builder allows any request to be passed in without the builder needing specific details of the request. The builder will simply invoke the createRequest method of the ApiQuery object to obtain the request object.
import org.http4k.client.ApacheClient
import org.http4k.core.Request
import org.http4k.core.Response
class ApiQueryBuilder {
private lateinit var apiQuery: ApiQuery
fun call(): Response {
check(::apiQuery.isInitialized) { "Query is undefined" }
val request: Request = apiQuery.createRequest()
val client = ApacheClient()
return client(request)
}
fun query(query: ApiQuery) {
this.apiQuery = query
}
}
The work is done in the call method of the ApiQueryBuilder. First, it performs checks to ensure that the required configuration is correct. Then, it invokes createRequest on the ApiQuery object, which was set through the query method to create the request object.
Finally, it creates the ApacheClient and invokes it with the request object and returns the response object.
Now that I have the builder, all I need to do is create a lambda-with-receiver to enable a DSL syntax:
fun apiTest(request: ApiQueryBuilder.() -> Unit): Response {
return ApiQueryBuilder().apply(request).call()
}
Now that all these fundamental pieces are in place it’s possible to write a test using the DSL.
I’ll start by making an HTTP request to ipinfo.io to obtain geographic details about an IP address. For this example, I’m using a Google DNS address, “8.8.8.8”.
import kotlin.test.Test
import org.http4k.core.Method
import org.http4k.core.Request
class DslTest {
@Test
fun `get IP info`() {
apiTest {
ipInfo("8.8.8.8")
}
}
}
fun ApiQueryBuilder.ipInfo(address: String) {
this.query { Request(Method.GET, "https://ipinfo.io/$address/geo") }
}
The extension function ipInfo adds a function to ApiQueryBuilder, which uses the query method of the class to set the Request object. When this test is run it makes the request and completes successfully. Now I need to add some assertions so I can verify the behaviour of the API I am calling.
You may have noticed that the function apiTest returns the Response object from the request that was made. That allows me to define an infix extension function on the Response class and add verification of the response to the DSL.
infix fun Response.should(verifyLambda: MatcherInvoker.() -> Unit) {
verifyLambda(MatcherInvoker(this))
}
class MatcherInvoker(private val response: Response) {
fun haveStatus(status: Status) {
response shouldHaveStatus(status)
}
}
Proxying through MatcherInvoker makes the DSL look nicer, even though http4k defines its own infix extension functions on the Response class. Otherwise, it would be more repetitious, similar to:
response should haveStatus(OK)
response should haveHeader("Content-Type", "application/json")
response should haveBody("{}")
However, that approach does mean that I’ll have to fill MatcherInvoker with functions to proxy those from http4k. Since this only needs to be done once and as I’ll be shipping this as a library, the extra effort is minimal for a much better-looking result.
With everything in place, I can run my first test using my brand-new DSL!
@Test
fun `get IP info`() {
apiTest {
ipInfo("8.8.8.8")
} should {
haveStatus(OK)
}
}
This is the base of a Kotlin DSL, which I can use to write API tests.
Adding extra query setup (e.g. headers, parameters, etc) and response verification (e.g. checking parts of the JSON body) is done through adding extra functions to ApiQueryBuilder and MatcherInvoker with the process established above.
As you can see, once you understand a small handful of Kotlin language constructs, it is fairly easy to write a DSL in the language. I hope this has inspired you to explore the possibility of creating your own DSL in Kotlin.
Wayne Bagguley
Senior Software Engineer
Wayne has been with Matillion for over four years and has recently been designing and implementing Spring-Boot microservices for the Data Productivity Cloud.
Featured Resources
What Is Massively Parallel Processing (MPP)? How It Powers Modern Cloud Data Platforms
Massively Parallel Processing (often referred to as simply MPP) is the architectural backbone that powers modern cloud data ...
BlogETL and SQL: How They Work Together in Modern Data Integration
Explore how SQL and ETL power modern data workflows, when to use SQL scripts vs ETL tools, and how Matillion blends automation ...
WhitepapersUnlocking Data Productivity: A DataOps Guide for High-performance Data Teams
Download the DataOps White Paper today and start building data pipelines that are scalable, reliable, and built for success.
Share: