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))
}
}
}
The above snippet produces the following result:
query theQuery {
allUsers(type: "users", limit: 10) {
name
address {
city
street(postCode: true)
}
}
}
The main challenges I have faced up to this point have been around supporting:
"allUsers"
)to
method for now)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")
}
I’ve achieved that thanks to the Invoke operator overloading support. Read on to find out how I have implemented it.
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)
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.
"allUsers" {
select("name")
select("title")
}
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.
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)
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.
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.
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
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))
}
}
The to method that allows for that comes from the standard library.
public infix fun A.to(that: B): Pair = Pair(this, that)
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
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 String.invoke(vararg args: Pair, block: (Field.Builder.() -> Unit)? = null): Field.Builder
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.
We are a leading niche digital & tech recruitment specialist for the North East of England. We Specialise in the acquisition of high-performing technology talent across a variety of IT sectors including Digital & Technology Software Development.
Our ultimate goal is to make a positive impact on every client and candidate we serve - from the initial call and introduction, right up to the final delivery, we want our clients and candidates to feel they have had a beneficial and productive experience.
If you’re looking to start your journey in sourcing talent or find your dream job, you’ll need a passionate, motivated team of experts to guide you. Check out our Jobs page for open vacancies. If interested, contact us or call 0191 620 0123 for a quick chat with our team.
Follow us on our blog, Facebook, LinkedIn, Twitter or Instagram to follow industry news, events, success stories and new blogs releases.
Back to Blog