Writing a GraphQL DSL in Kotlin

Alessandro Diaferia - Mar 18 '19 - - Dev Community

I’ve recently spent some time testing a GraphQL endpoint against a few queries. At the moment I’m keeping my queries as multi-line strings but I was wondering:

How hard would it be to build a GraphQL query DSL in Kotlin?

I thought this could be a good opportunity to become more familiar with Kotlin DSL capabilities.

Here’s what I’ve got so far.

query("theQuery") { 
    "allUsers"("type" to "users", "limit" to 10) {
        select("name")
        select("address") {
            select("city")
            select("street"("postCode" to true))
        } 
    }
}
Enter fullscreen mode Exit fullscreen mode

The above snippet produces the following result:

query theQuery {
    allUsers(type: "users", limit: 10) { 
        name
        address { 
            city 
            street(postCode: true)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The main challenges I have faced up to this point have been around supporting:

  • any string to be used as the root field of the query (e.g."allUsers")
  • nested selection of fields
  • a map-like syntax for field arguments (I’ve settled for the to method for now)

Any String is a Field

As you can see from the above example, it is possible to start the root field declaration using a string, followed by the fields selection:

"allUsers" { 
    select("name") 
    select("title")
}
Enter fullscreen mode Exit fullscreen mode

I’ve achieved that thanks to the Invoke operator overloading support. Read on to find out how I have implemented it.

String.invoke to the rescue

The incredibly powerful extensions support helps me define my own implementation of invoke on a String.

operator fun String.invoke(block: Field.Builder.() -> Unit) = Field.Builder().apply { this@apply.block() }.withName(this)
Enter fullscreen mode Exit fullscreen mode

This way, any String instance can be turned into a Field.Builder by passing a block to the invoke operator (). Additionally, Kotlin’s compact syntax, saves us from having to explicitly use the open and close parenthesis, making the result a little more readable.

Select-ing sub-fields

"allUsers" { 
    select("name") 
    select("title")
}
Enter fullscreen mode Exit fullscreen mode

Inside the declared root field, a sequence of select instructions informs the current field builder about which sub-fields we are interested in. The way this is achieved is by letting the compiler know that we are in the context of a Field.Builder and that any method specified in the block has to be resolved against it. This is possible thanks to function literals with receiver.

Function literals with receiver

This is probably the most useful feature Kotlin has to offer when it comes to building DSLs.

operator fun String.invoke(block: Field.Builder.() -> Unit)
Enter fullscreen mode Exit fullscreen mode

The block argument has been declared as Field.Builder.() -> Unit.

As we can see from the docs:

[…] Kotlin provides the ability to call an instance of a function type with receiver providing the receiver object.

Function literals with receiver – Kotlin reference

What this means is that I can invoke the block having the current Field.Builder instance as receiver resulting in the select invocations to be being resolved against it.

Field arguments

When it comes to specifying field arguments, I’ve had to settle for that not-so-pretty to syntax.

"type" to "users", "limit" to 10
Enter fullscreen mode Exit fullscreen mode

I still think it’s a good compromise considering that Kotlin doesn’t offer much more when it comes to map-building syntax.

"allUsers"("type" to "users", "limit" to 10) {
    select("name")
    select("address") {
        select("city")
        select("street"("postCode" to true)) 
    }
}
Enter fullscreen mode Exit fullscreen mode

The to method that allows for that comes from the standard library.

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
Enter fullscreen mode Exit fullscreen mode

Note that the infix keyword is what allows for the simplified notation receiver method argument.

Finally, a slightly more complicated definition of String.invoke accepts instances of Pair<String, T> allowing for the to syntax to be used when specifying field arguments. The explicit String type as the left type helps keeping it all a little more robust.

operator fun <T> String.invoke(vararg args: Pair<String, T>, block: (Field.Builder.() -> Unit)? = null): Field.Builder
Enter fullscreen mode Exit fullscreen mode

Wrapping up

As you can see, I’m not a DSL expert (at all!) but this is a fun experiment to play with. You can follow my work at the graphql-forger repo. Please, feel free to contribute by opening issues or pull requests.

I hope you have enjoyed the post and learnt something new about Kotlin.

The post Writing a GraphQL DSL in Kotlin appeared first on Ale's main thread.

GitHub logo alediaferia / graphql-forger

A simple and fast GraphQL Query builder written in Kotlin

GraphQLForgr

A simple and fast GraphQL query builder written in Kotlin

Status

This project is EXPERIMENTAL and not ready for production use.

Contributing

Feel free to help out by opening issues or pull requests.

License

This project is Open Source Software under the terms of the MIT License






. . . . . . . . . . . . . . . . . . . . . .