Generating a GraphQL Query Kotlin DSL with Annotations
source link: https://proandroiddev.com/generating-a-graphql-query-dsl-with-kotlin-b4bfc9def9a8
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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
AnnotationCodegen 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:
- We add the annotation project we created earlier so it’s visible in the codegen project.
- We add the
auto.service
dependencies that are responsible for the annotation processing logic. - 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
The final code should look like this:
query processor classBefore we move further, we need to create another class called QueryInfo:
QueryInfoA 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
QueryInfo creatorThen we generate the actual file from the query info 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 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:
query processor with generatorsClasses 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:
process functionNow 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.
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.
Generated CodeLet'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:
Generated CodeHow do we generate this with KotlinPoet?
KotlinPoet CodeLet’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 id\r\n
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:
Generated Code1. 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:
KotlinPoet Code- 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
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.
KotlinPoet code for thos will be:
Kotlin Poet CodeString 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.
Generated CodeKotlinPoet:
Kotlin Poet CodeBuild the query class:
each section of the code inside the QueryBuilder class is warped inside an extension function for the TypeSpec.classBuilder
KotlinPoet function
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:
DSL builderThis will generate:
Generated CodeRemember 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.
from the QueryProcessor classUsage
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
Annotation usage example3. build your project, the generated files should be created
2. Then write a query anywhere in your app module code
Generated DSL usage example3. 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.
The setArguments
function accepts vararg
of type Triple created by this function which lives inside the util module:
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.
- Start by generating a new StringBuilder for arguments
- Add arguments section starting with
(
- Add a filter section, it typically starts with
filter:{
- loop through the arg array, adding them one by one with this convention:
name : value
- close the filter and arguments section
- 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:
Generated CodeKotlinPoet:
Kotlin Poet CodeWhen using this function like this
adding argumentsIt’ll generate this query string
{"query":"query {\r\n characters (filter:{ name : \"value\",id : 1 }) {\r\n results {\r\n id\r\n name\r\n }\r\n }\r\n }\r\n "}
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK