Generating a GraphQL Query DSL with Kotlin
Tags: graphql, kotlin, android
Published: 2021-04-06 17:06:19



###### This is an experiment trying to utilize Kotlin DSL and annotation processing to write queries for GraphQL, I used KotlinPoet as a code generator and Android to test the results but the code is pure Kotlin.

##### The idea came from a colleague who suggested that we could use the same common project setup we use for most projects that consists of Retrofit/OKHttp as a REST client as a GraphQLClient instead of relying on heavy dependences like ApolloClient.

#### Here is what the query looked like at the end:
```kotlin
    val query = charactersQuery{
        results{
            id
            name
        }
    }
```
This DSL is a CharactersQuery; a generated class from Kotlin Data Class, it can generate a valid GraphQL JSON query to be submitted(as RequestBody) through POST request to GraphQL server.
 
###### For me this code looks really easy to read and understand with slight familiarity with GraphQL query syntax, mostly I'll go through the generated code with some of KotlinPoet code that I found interesting to write. 

You can find the complete code here: [github/KLQuery](https://github.com/mtzhisham/KLQuery)

Or even try it now:

{% gist
https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=download.gradle %}

### 1- Starting with the query annotation 

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=KLQuery.kt %}

This is a class annotation and the only one in the code, it's used to annotate data classes to specify that this class will be used as a query.
 
### 2- Query info

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=QueryInfo.kt %}

A data class that holds the query info that will come in handy for passing the necessary information from the processor to the builder.

How is is generated?, we pass the annotation element and extract the needed values form it.  

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=generateQueryInfo.kt %}

### 3- The query annotation processor

This is a processor class that is annotated with `@AutoService(Processor::class)` and extends `AbstractProcessor()` typical for annotation processor classes expect with one small addition.

We specify that the annotation we will work with 

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=getSupportedAnnotationTypes.kt %}

Then we override the `process` method that is responsible for processing the annotation elements.

From the `roundEnv` we get the elements annotated with KLQuery `roundEnv.getElementsAnnotatedWith(KLQuery::class.java)`
and then we check if this element is in fact a class else we print an error 

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=annotationError.kt %}

We generate query info from the method in the last step.

Then we generate the actual file and write it to the `kapt.kotlin.generated` directory 

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=buildFileSpec.kt %}

A couple of important things is happening here; first we add a generated class to this file by `addType(QueryBuilder(queryInfo).buildClass())`

Then we add a DSL method to the file outside of the class by 
`addFunction(QueryBuilder(queryInfo).buildDSL())`

#### 3.1- Classes with declared types

Last step would have been enough if your classes only contains  non-declared types meaning that while a class is being generated it could as well contain a type of generated class that has not been generated yet resulting in the processing task to fail.

A quick work around that I came with on the spot is to check if the current element that being processed contains declared types if it does add it to a queue and skip it until all elements with non declared typed is processed then process the queue elements until it's empty. -a better approach can be added here-

The final code for processing elements should look like: 

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=declaredElements.kt %}

Now for the code for `hasDeclaredTypes` implementation: 

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=hasDeclaredTypes.kt %}

By this far we are ready to build and generate the actual query class that we'll use in our code.

### 4-The query builder 
The backing implementation of the query class is basically a StringBuilder with each value being written inside the method block is appending to the builder 

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=CharactersQuery.kt %}

### 5- Lets start with the class fields 
The general idea for creating a DSL that looks like JavaScript code wile maintaining Kotlin statically typed nature this was done by using this pattern:

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=classFields.kt %}

Let's go through the code:
1. We generate a class field with the same name we found in the original class but the type is irrelevant so it's replace with Unit
2. We override the getter method to add out custom implementation, basically we want this method to be called each time that field is written inside the DSL method 
3. The actual method implementation is written with _ prefix since we are not interested in this method  

The above pattern allows us to write `id` inside a DSL method and expect `id\r\n ` to be appended to the query StringBuilder

##### How do we generate this with KotlinPoet?

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=classFieldGenerator.kt %}

_Pretty straight forward I'd say._

#### How about declared types?

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=declaredTypes.kt %}

1. We generate a class field of the generated declared type, this is private because we are not interested in using it in our code
2. we generate an extension that will help us write this field as a DSL method in our parent DSL 
3. We initialize the class field with a new instance and we pass the query to it so everything is written inside this block is appended to the original query 
4. We apply the written block to the instance 

The KotlinPoet code for the property is very similar to the one above, the method builder on the other hand has 2 new differences:

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=declaredTypesGenerator.kt %}

* First we specified a receiver for the extension by ` .receiver(ClassName(info.queryPackage, info.queryClassName))`

* And then we passed a lambda as a parameter to be the block  

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=ParameterSpec.kt %}

### 6- The constructor

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=constructor.kt %}

The constructor is doing 2 things here, 
If this query the parent query meaning it's the first one created then instantiate a StringBuilder query with GraphQL query convention `{\"query\":\"query"` but
if this query class is not a parent query meaning the the query object already created before this point -cascaded- so all the appending will be done on that query and no need to create a new one


### 7- String query builder method
{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=buildQueryString.kt %}

You call this when you want to generate a string from the query generated it closes the hierarchy based of if it's cascaded or not.

### 8- dsl builder

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=dslbuilder.kt %}

Here we generate the DSL method that will use if this query is a parent query.

### 9- Usage

1. In any data class: 

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=dataClasses.kt %}

2. Then write a query 

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=usage.kt %}

3. Then get the string 

```kotlin
query.buildQueryString()
``` 

### 10- How about some arguments? 
##### here I only implemented adding filter  to the query and it supports adding nested filters

If you are familiar with GraphQL queries then you might be used to the a `variables` object sent with the query that contains the filter values, here I kept away from using it and added the values directly inside the filter section after the filter name not to complicate things farther was the main reason for this.

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=arguments.kt %}

This method accepts a `vararg` of type Triple created by this method:

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=Stringto.kt %}

_`Matchers` is a sealed class that that has some types you can use directly and a Custom type that let's you provide your own filter syntax to be passed to the filters section, see the utility module in code to find more._

Here is a simplified version of how how the arguments is added: 

{% gist https://gist.github.com/mtzhisham/bc9aaa4c8ae9f57fdae8de018f4fd0c0 file=setArguments.kt %}

The last 3 lines are the most interesting thing about this method IMO, until before adding the filters the query looks something like this `...someName{\\r\\n` but we want to write it in the format `...someName(filters){\\r\\n` so what I did is removing this section `{\\r\\n` adding the filters section then adding the `{\\r\\n` again!

![Alt text of image](https://media.giphy.com/media/bg1MQ6IUVoVOM/giphy.gif)

And that's it by simply adding one annotation `@KLQurey` -I don't know how this name came to me it seems lazy but I kind like it- to a class you generate very cool methods that might speed up your GraphQL consuming with maybe your existing REST implementation.
What I realy like about this is how scalable the generator code can be and this thanks to Kotlin and KotlinPoet. Now I'm not an expert in any of the topics above-not even close- but it is always fun to experiment and make something with Kotlin.


 _Cover Photo by <a href="https://unsplash.com/@wawa01?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Waren Brasse</a> on <a href="https://unsplash.com/s/photos/tree?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>_