DEV Community

loading...
Cover image for Generating a GraphQL Query DSL with Kotlin

Generating a GraphQL Query DSL with Kotlin

Moataz Hisham
Android Software Engineer
・6 min read
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:

    val query = charactersQuery{
        results{
            id
            name
        }
    }
Enter fullscreen mode Exit fullscreen mode

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

Or even try it now:

1- Starting with the query annotation

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

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.

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

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

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

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:

Now for the code for hasDeclaredTypes implementation:

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

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:

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?

Pretty straight forward I'd say.

How about declared types?

  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:

  • 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

6- The constructor

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

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

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

9- Usage

  1. In any data class:
  1. Then write a query
  1. Then get the string
query.buildQueryString()
Enter fullscreen mode Exit fullscreen mode

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.

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

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:

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

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 Waren Brasse on Unsplash

Discussion (0)

Forem Open with the Forem app