Jerkey: A Kotlin DSL for Jersey
Wednesday, April 11, 2018 |I’m currently working on a DSLs-in-Kotlin presentation for my local JUG, so I need a good domain in which to work. HTML is a great sample domain, but it’s been done to death. After a bit of head scratching, I’ve come up with what is, I think, a somewhat novel domain: REST application building. Sure, there are libraries like Ktor, but suffers from some very serious NIH. I’m totally kidding, but the dearth of discussions regarding REST applications and DSL construction was good enough for me, so let’s see what we have so far.
What really sold me on the idea was that Jersey already offers an API for programmatically creating REST endpoints, which you can read about here. All we need to do then, is define a DSL to build the application model, then run it through this API and let Jersey do the heavy lifting, which sounds perfect for what is intended to be, primarily, a didactic project. I’ll spare you the details of how the DSL is built and skip straight to the "finished" project:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
application {
produces = "application/json"
consumes = "application/json"
resource {
path = "items"
method = "get"
handler = ::listItems
param {
source = "query"
name = "someParam"
type = Int::class
}
}
resource {
path = "items/{id}"
method = "get"
handler = ::getItem
param {
source = "path"
name = "id"
type = Int::class
}
}
}.build()
While there’s a good chance I’ll modify the structure of the DSL as I continue to work on the presentation, this
represents a working DSL. Once I call build()
, I can then access the REST application via a browser or curl.
A few things to call out. Notice the handler
property. With that, I can specify the function in my Kotlin code
that will actually handle the request. Where Jersey allows me to define a method like this:
1
public Response listItems(@QueryParam("someParam") Integer someParam) { ... }
I have yet to be able to figure out how to dispatch to a Kotlin method with an arbitrary number of parameters. One
might think of the spread operator, but requires the receiving method specify a varargs
parameter, which I’ve tried to
avoid, possibly for no good reason other than tunnel vision. :) What I’ve opted to use, though, is CallContext
object,
which will encapsulate various things pulled from the request and presented in, in theory, ready-to-use forms. In this
instance, the context would have a parameter called 'someParam' of type Int
. At this point, the type coercion is pretty
crude, but the whole thing is a work in progress, so cut me some slack. :)
One of the more interesting parts, I think, is the creation of the resources for Jersey to consume, and part of the fun in that is the demonstration of Kotlin’s Java interop, going both directions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class JerkeyResourceConfig(val application: Application) : ResourceConfig() {
init {
val resourceBuilder = Resource.builder()
resourceBuilder.path(application.path)
application.resources.forEach { res ->
val childResource = resourceBuilder.addChildResource(res.path)
val methodBuilder = childResource.addMethod(res.method.toUpperCase())
methodBuilder.produces(res.produces)
.handledBy(Inflector<ContainerRequestContext, Response> {
val context = CallContext()
(it.uriInfo.queryParameters + it.uriInfo.pathParameters).forEach { qp ->
val name = qp.key!!
val value = qp.value[0]!!
val param = res.params[name]
param?.let {
context.processParam(param, name, value)
}
}
res.handler?.invoke(context)
})
}
registerResources(resourceBuilder.build())
}
}
We iterate over the resource
instances from the DSL, creating subresources of the
base resource defined by the application
element in the DSL.
My DSL code consumes that here:
1
2
3
4
5
6
7
8
fun build() {
val rc = JerkeyResourceConfig(this)
val baseUri = UriBuilder.fromUri("http://localhost$path").port(port).build()
val server = JdkHttpServerFactory.createHttpServer(baseUri, rc);
readLine()
server.stop(0)
}
Put all of that together with this handler method:
1
2
3
4
fun listItems(context : CallContext) : Response {
val id : Int = context.params["param"] as Int
return Response.ok().entity("items $id").build()
}
and we get this from the command-line:
1
2
$ curl -Ssk http://localhost:8080/items?param=1024
items 1024
It’s not very flashy, but for one evening’s hacking, it’s not too shabby. :) If you’d like to follow along, you can find the (meager) sources here.