Categories
Studio

Generating a GraphQL Query Kotlin DSL with Annotations

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.

Photo by Waren Brasse on Unsplash

Trying to use GraphQL in Android can be intimidating if you use some of the available client libraries with almost all of them depending on a specific project structure that can be demanding in resources and could limit you to make some decisions based on their capability instead of the other way around.

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

Here is what the query looked like at the end:

The final code

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 a POST request to a GraphQL server.

For me, this code looks really easy to read and understand with slight familiarity with GraphQL query syntax.

We’ll discuss three topics: Annotation processing with KotlinPoet as a code generator, Kotlin DSLs, and GraphQL query syntax. It’ll be helpful if you are somewhat familiar with them, but I don’t think it's necessary. I’ll go through the generated code with some of the KotlinPoet code that I found interesting to write.

You can find the complete code here:

mtzhisham/KLQuery

Let’s get to it, Create a new App in Android Studio and take a look at the project structure, we’ll discuss each module as we go.

Project structure:

In this project, we have 4 main modules

  1. Annotation module: Kotlin library hosts an annotation class.
  2. Codegen module: Kotlin library responsible for all the code generation and annotation processing.
  3. Util module: Kotlin library has utility classes used by the codegen and app modules.
  4. App module: helpful for testing the implementation and providing sample usage.
Project structure

Annotation module:

In Android Studio, add a new module to your project and add choose Java or Kotlin library.
This module will contain one class, create an annotation class and give it a name, this class will be used to annotate data classes to specify that this class will be used as a query.

Specify that you want this annotation to be used only with classes and the will only be available in source code

https://medium.com/media/5aee978828b9ab0476e843230270fab2/href

Codegen module:

This module also a Kotlin library, add a new module to your project and add choose Java or Kotlin library. We’ll do most of the work required to process the annotation we created and generate the required classes and functions.

add this plugin to the list of module plugins

plugins {
id 'kotlin-kapt'
}

Here we specify that we’ll use annotations in our module.

Then in the build.gradle file we’ll add these dependencies:

https://medium.com/media/d492532507aefc25e5a79fbeb280f486/href

  1. We add the annotation project we created earlier so it’s visible in the codegen project.
  2. We add the auto.service dependencies that are responsible for the annotation processing logic.
  3. We add KotlinPoet to help us with code generation.

Create the query annotation processor:

@AutoService(Processor::class)
class QueryProcessor: AbstractProcessor() {

This is a processor class that is annotated with @AutoService(Processor::class) and extends AbstractProcessor()

Here we specify that this class needs to be processed during compilation to generate the necessary code.

The implementation is typical for annotation processor classes except with one small addition.

We specify the annotation we will work with by overriding getSupportedAnnotationTypes and passing the query annotation class we created.

override fun getSupportedAnnotationTypes(): MutableSet<String> = mutableSetOf(KLQuery::class.java.name)

Then we override process() the function that is responsible for processing the annotation elements.

override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {...

Specify the generated files location, in this case, we’ll use kapt.kotlin.generated

val kaptKotlinGeneratedDir = processingEnv.options["kapt.kotlin.generated"] ?: return false

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

https://medium.com/media/0a23b020938a2753cad66444eaf30445/href

The final code should look like this:

https://medium.com/media/77485e859c53857a07dd61807d83bdd6/href

Before we move further, we need to create another class called QueryInfo:

https://medium.com/media/beeec7379d732e4d2a2adbc4990bb80c/href

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

Now inside the QueryProcessor class, create a function and pass the annotation element from the process function and extract the needed values from it.

What are we looking for is the element class passed, its name and the package name

https://medium.com/media/9449b4ef3044e58b3af852d5470f239e/href

Then we generate the actual file from the query info and write it to the kapt.kotlin.generated directory

https://medium.com/media/a167d918a345c3cd087444ec44a397c5/href

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 function to the file outside of the class by
addFunction(QueryBuilder(queryInfo).buildDSL())

we’ll go through each function implementation when we work on the builder class.

This class now looks like this:

https://medium.com/media/d57862753e298e687f54ab1907fd5ad8/href

Classes with declared types

The last step would have been enough if your classes only contain non-declared types, meaning that while a class is being generated, it could contain a type of generated class that has not been generated yet, resulting in the processing task failing.

A quick workaround is to check if the currently processed element contains declared types. If it does, add it to a queue and skip it until all non-declared typed elements are 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 this:

https://medium.com/media/fceb6a860630ce052c68d417785073b8/href

Now for the code for hasDeclaredTypes implementation:

https://medium.com/media/456cf3dea43c9c3c25d03b35d0eb27c6/href

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

Building a Query class

The query class's backing implementation is basically a StringBuilder, with each value being written inside the DSL function block is appended to the query instance.

https://medium.com/media/8fd5f084a26118ced7389203dd8ed224/href

Let's start with the class fields.

The general idea for creating a DSL “that looks like JavaScript code" while maintaining Kotlin statically typed nature was done by using this pattern:

https://medium.com/media/bb437fd2ab072def1e563613f773b980/href

How do we generate this with KotlinPoet?

https://medium.com/media/58c4d91ad871174ee4bde88e60f486b9/href

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 replaced with ‘Unit.’
2. We override the getter function to add our custom implementation. Basically, we want this function to be called each time that field is written inside the DSL function.
3. The actual function implementation is written with _ prefix since we are not interested in this function

The above pattern allows us to write id inside a DSL function and expect idrn to be appended to the query StringBuilder

What about declared types?

The KotlinPoet code for the declared property is very similar to the one above. The function builder, on the other hand, has differences:

https://medium.com/media/10930e0ca48958e97ca4c28b8ee02d62/href

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 outside of this class.
2. we generate an extension that will help us write this field as a DSL function 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.

Now let’s generate it with KotlinPoet:

https://medium.com/media/9e5e8481970d92b9dca58e6a8df8d1ae/href

  • 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

https://medium.com/media/3b9344bf4f31732ac1e2615a645788cd/href

Next, we need to add a constructor to the query class:

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” If this query class is not a parent query meaning 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.

https://medium.com/media/03a02c192089a324274c15afe2166129/href

KotlinPoet code for thos will be:

https://medium.com/media/4902ea713b35e9381505873476f69ee4/href

String query builder function

Here we close the quires hierarchy; close the closest opened nested fields opened if this query is cascaded, close the first and second query sections.

https://medium.com/media/56f1f3f45446d2a753384cbbf2ff0521/href

KotlinPoet:

https://medium.com/media/cfe17d9371ca7fc6c7429300156cbd19/href

Build the query class:

each section of the code inside the QueryBuilder class is warped inside an extension function for the TypeSpec.classBuilder KotlinPoet function

https://medium.com/media/70eb140f76a51ca52a6b0a622c2614c0/href

DSL Builder

Now the star of all of this is the DSL function.

You create a function with a lambda parameter type argument that is the block, the receiver is the current query class and no parameters and Unit return type.

Add one statement, applying the block to the query class and return it; this returned class is the query class that generates the query string and is used to add other parameters and fields in the app code.

Here is how it’s generated:

https://medium.com/media/5524e42d9565bcef7eef58bbde123a29/href

This will generate:

https://medium.com/media/9ca63d646182c3fc493ef2cf0d522ffd/href

Remember how we use these functions inside the processor class?

First, build and add the class itself, then add the DSL function to the file; note that the function will be added outside of the class so it can be used anywhere without the need of creating an instance of that class.

https://medium.com/media/a167d918a345c3cd087444ec44a397c5/href

Usage

1. In any data class, annotate it with the annotation you created, note that every data class variable inside this data class also needs to be annotated

https://medium.com/media/0de57285f36e4eb55aacdea5ab23bcf3/href

3. build your project, the generated files should be created

2. Then write a query anywhere in your app module code

https://medium.com/media/a111446259b9c9ee65dbeabe380ce361/href

3. Then get the string

query.buildQueryString() , this will generate the following string:

{"query":"query {\r\n charactersQuery {\r\n results {\r\n id\r\n name\r\n  }\r\n  }\r\n }\r\n  "}

Pass this as a RequestBody through retrofit and you are done!

10- How about some arguments?

Here well discuess adding filter to the query and it supports adding nested filters, but using the same steps any other type of arguments can be added.

If you are familiar with GraphQL queries, then you might be used to the 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 further was the main reason for this.

https://medium.com/media/4f0cbc1422187d5f05d3275c673e0822/href

The setArguments function accepts vararg of type Triple created by this function which lives inside the util module:

https://medium.com/media/e612c2d4e2f1b3c266307dff56358aaf/href

Matchers is a sealed class that has some types you can use directly and Custom “infix types” like to, eq and match that lets you provide your own filter syntax to be passed to the filters section. See the util module in the code to find more.

  1. Start by generating a new StringBuilder for arguments
  2. Add arguments section starting with (
  3. Add a filter section, it typically starts with filter:{
  4. loop through the arg array, adding them one by one with this convention: name : value
  5. close the filter and arguments section
  6. Append them to the original query

The last 3 lines are the most interesting thing about this function 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!

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

https://medium.com/media/05ba9bb39854a55a6091c2c67dd74cb3/href

KotlinPoet:

https://medium.com/media/a98ab6640a611679e9ef2a7d0104e085/href

When using this function like this

https://medium.com/media/0e2cdf0f5d895d119826a9594ba8747f/href

It’ll generate this query string

{"query":"query {rn characters  (filter:{ name : "value",id : 1 }) {rn results {rn idrn namern  }rn  }rn }rn  "}

https://medium.com/media/667bd6cc5b005f4c8a291be1fdd6ba2d/href

And that’s it by simply adding one annotation. to a data class you generated very cool functions that might speed up your GraphQL consumption with maybe your existing REST implementation.
What I really like about this is how scalable the generator code can be, 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.


Generating a GraphQL Query Kotlin DSL with Annotations was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.

Source

Leave a Reply

Your email address will not be published. Required fields are marked *