Skip to content

Commit 667297f

Browse files
authored
[plugin] update client generator to support kotlinx serialization (#1069)
This is a second PR in a series to introduce support for both `Jackson` and `kotlinx.serialization` data formats for GraphQL Kotlin clients. This PR updates `graphql-kotlin-client-generator` to allow generating data models supporting both formats. Due to the declaration limitations of Kotlin sealed classes (in Kotlin 1.4 sealed classes and their implementations have to be a top level definitions), generation logic was also updated to generate operation specific data models under package name matching that operation name. Common objects such as input objects, enums and custom scalars definitions will be shared across different queries. Large size of PR is driven primarily with test cases - instead of comparing generated classes against a hard coded Strings, tests were updated to compare it against files. Generator tests were also updated to verify generated sources can be compiled. * #1048 - this is an attempt to split it up to more manageable pieces * #929 - add support for generating `kotlinx.serialization` data models * #1066 - first PR in the series
1 parent a9ad9fc commit 667297f

File tree

221 files changed

+4333
-3461
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

221 files changed

+4333
-3461
lines changed

examples/client/gradle-client/build.gradle.kts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import com.expediagroup.graphql.plugin.gradle.config.GraphQLClientType
21
import com.expediagroup.graphql.plugin.gradle.config.GraphQLScalar
32
import com.expediagroup.graphql.plugin.gradle.graphql
43

@@ -31,7 +30,6 @@ graphql {
3130
allowDeprecatedFields = true
3231
headers = mapOf("X-Custom-Header" to "My-Custom-Header")
3332
customScalars = listOf(GraphQLScalar("UUID", "java.util.UUID", "com.expediagroup.graphql.examples.client.gradle.UUIDScalarConverter"))
34-
clientType = GraphQLClientType.KTOR
3533
}
3634
}
3735
ktlint {

examples/client/gradle-client/src/main/kotlin/com/expediagroup/graphql/examples/client/gradle/Application.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import com.expediagroup.graphql.generated.ExampleQuery
2222
import com.expediagroup.graphql.generated.HelloWorldQuery
2323
import com.expediagroup.graphql.generated.RetrieveObjectQuery
2424
import com.expediagroup.graphql.generated.UpdateObjectMutation
25+
import com.expediagroup.graphql.generated.inputs.BasicObjectInput
26+
import com.expediagroup.graphql.generated.inputs.SimpleArgumentInput
2527
import io.ktor.client.HttpClient
2628
import io.ktor.client.engine.okhttp.OkHttp
2729
import io.ktor.client.features.logging.DEFAULT
@@ -71,16 +73,16 @@ fun main() {
7173
val retrieveNonExistentObject = client.execute(RetrieveObjectQuery(variables = RetrieveObjectQuery.Variables(id = 1)))
7274
println("\tretrieve non existent object: ${retrieveNonExistentObject.data?.retrieveBasicObject}")
7375

74-
val addResult = client.execute(AddObjectMutation(variables = AddObjectMutation.Variables(newObject = AddObjectMutation.BasicObjectInput(1, "first"))))
76+
val addResult = client.execute(AddObjectMutation(variables = AddObjectMutation.Variables(newObject = BasicObjectInput(1, "first"))))
7577
println("\tadd new object: ${addResult.data?.addBasicObject}")
7678

77-
val updateResult = client.execute(UpdateObjectMutation(variables = UpdateObjectMutation.Variables(updatedObject = UpdateObjectMutation.BasicObjectInput(1, "updated"))))
79+
val updateResult = client.execute(UpdateObjectMutation(variables = UpdateObjectMutation.Variables(updatedObject = BasicObjectInput(1, "updated"))))
7880
println("\tupdate new object: ${updateResult.data?.updateBasicObject}")
7981
}
8082

8183
println("additional examples")
8284
runBlocking {
83-
val exampleData = client.execute(ExampleQuery(variables = ExampleQuery.Variables(simpleCriteria = ExampleQuery.SimpleArgumentInput(max = 1.0f))))
85+
val exampleData = client.execute(ExampleQuery(variables = ExampleQuery.Variables(simpleCriteria = SimpleArgumentInput(max = 1.0f))))
8486
println("\tretrieved interface: ${exampleData.data?.interfaceQuery} ")
8587
println("\tretrieved union: ${exampleData.data?.unionQuery} ")
8688
println("\tretrieved enum: ${exampleData.data?.enumQuery} ")

examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/Application.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import com.expediagroup.graphql.generated.ExampleQuery
2222
import com.expediagroup.graphql.generated.HelloWorldQuery
2323
import com.expediagroup.graphql.generated.RetrieveObjectQuery
2424
import com.expediagroup.graphql.generated.UpdateObjectMutation
25+
import com.expediagroup.graphql.generated.inputs.BasicObjectInput
26+
import com.expediagroup.graphql.generated.inputs.SimpleArgumentInput
2527
import io.netty.channel.ChannelOption
2628
import kotlinx.coroutines.runBlocking
2729
import org.springframework.http.client.reactive.ClientHttpConnector
@@ -64,16 +66,16 @@ fun main() {
6466
val retrieveNonExistentObject = client.execute(RetrieveObjectQuery(variables = RetrieveObjectQuery.Variables(id = 1)))
6567
println("\tretrieve non existent object: ${retrieveNonExistentObject.data?.retrieveBasicObject}")
6668

67-
val addResult = client.execute(AddObjectMutation(variables = AddObjectMutation.Variables(newObject = AddObjectMutation.BasicObjectInput(1, "first"))))
69+
val addResult = client.execute(AddObjectMutation(variables = AddObjectMutation.Variables(newObject = BasicObjectInput(1, "first"))))
6870
println("\tadd new object: ${addResult.data?.addBasicObject}")
6971

70-
val updateResult = client.execute(UpdateObjectMutation(variables = UpdateObjectMutation.Variables(updatedObject = UpdateObjectMutation.BasicObjectInput(1, "updated"))))
72+
val updateResult = client.execute(UpdateObjectMutation(variables = UpdateObjectMutation.Variables(updatedObject = BasicObjectInput(1, "updated"))))
7173
println("\tupdate new object: ${updateResult.data?.updateBasicObject}")
7274
}
7375

7476
println("additional examples")
7577
runBlocking {
76-
val exampleData = client.execute(ExampleQuery(variables = ExampleQuery.Variables(simpleCriteria = ExampleQuery.SimpleArgumentInput(max = 1.0f))))
78+
val exampleData = client.execute(ExampleQuery(variables = ExampleQuery.Variables(simpleCriteria = SimpleArgumentInput(max = 1.0f))))
7779
println("\tretrieved interface: ${exampleData.data?.interfaceQuery} ")
7880
println("\tretrieved union: ${exampleData.data?.unionQuery} ")
7981
println("\tretrieved enum: ${exampleData.data?.enumQuery} ")

plugins/client/graphql-kotlin-client-generator/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,5 @@ using [square/kotlinpoet](https://github.com/square/kotlinpoet) library.
2727

2828
## Code Generation Limitations
2929

30-
* Due to the custom logic required for deserialization of polymorphic types and default enum values only Jackson is currently supported.
3130
* Only a single operation per GraphQL query file is supported.
3231
* Subscriptions are currently NOT supported.

plugins/client/graphql-kotlin-client-generator/build.gradle.kts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
description = "GraphQL Kotlin common utilities to generate a client."
22

3+
val compileTestingVersion: String by project
34
val graphQLJavaVersion: String by project
5+
val junitVersion: String by project
46
val kotlinPoetVersion: String by project
7+
val kotlinxSerializationVersion: String by project
58
val ktorVersion: String by project
69
val wireMockVersion: String by project
710

811
dependencies {
9-
api(project(path = ":graphql-kotlin-ktor-client"))
10-
api(project(path = ":graphql-kotlin-spring-client")) {
11-
exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-reactor")
12-
exclude(group = "org.springframework")
13-
exclude(group = "org.springframework.boot")
14-
}
12+
api(project(path = ":graphql-kotlin-client"))
1513
api("com.graphql-java:graphql-java:$graphQLJavaVersion") {
1614
exclude(group = "com.graphql-java", module = "java-dataloader")
1715
}
1816
api("com.squareup:kotlinpoet:$kotlinPoetVersion")
17+
api("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion")
1918
implementation("io.ktor:ktor-client-cio:$ktorVersion")
2019
implementation("io.ktor:ktor-client-jackson:$ktorVersion")
2120
testImplementation("com.github.tomakehurst:wiremock-jre8:$wireMockVersion")
21+
testImplementation("com.github.tschuchortdev:kotlin-compile-testing:$compileTestingVersion")
22+
testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion")
2223
}
2324

2425
tasks {

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/downloadSchema.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,12 @@ import io.ktor.client.engine.cio.endpoint
2323
import io.ktor.client.features.ClientRequestException
2424
import io.ktor.client.request.get
2525
import io.ktor.client.request.header
26-
import io.ktor.util.KtorExperimentalAPI
2726
import kotlinx.coroutines.TimeoutCancellationException
2827
import kotlinx.coroutines.runBlocking
2928

3029
/**
3130
* Downloads GraphQL SDL from the specified endpoint and verifies whether the result is a valid GraphQL schema.
3231
*/
33-
@KtorExperimentalAPI
3432
fun downloadSchema(
3533
endpoint: String,
3634
httpHeaders: Map<String, Any> = emptyMap(),

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generateClient.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package com.expediagroup.graphql.plugin.client
1818

1919
import com.expediagroup.graphql.plugin.client.generator.GraphQLClientGenerator
2020
import com.expediagroup.graphql.plugin.client.generator.GraphQLClientGeneratorConfig
21-
import com.expediagroup.graphql.plugin.client.generator.GraphQLClientType
21+
import com.expediagroup.graphql.plugin.client.generator.GraphQLSerializer
2222
import com.expediagroup.graphql.plugin.client.generator.GraphQLScalar
2323
import com.squareup.kotlinpoet.FileSpec
2424
import graphql.schema.idl.SchemaParser
@@ -31,7 +31,7 @@ fun generateClient(
3131
packageName: String,
3232
allowDeprecated: Boolean = false,
3333
customScalarsMap: List<GraphQLScalar> = emptyList(),
34-
clientType: GraphQLClientType = GraphQLClientType.DEFAULT,
34+
serializer: GraphQLSerializer = GraphQLSerializer.JACKSON,
3535
schema: File,
3636
queries: List<File>
3737
): List<FileSpec> {
@@ -40,7 +40,7 @@ fun generateClient(
4040
packageName = packageName,
4141
allowDeprecated = allowDeprecated,
4242
customScalarMap = customScalars,
43-
clientType = clientType
43+
serializer = serializer
4444
)
4545
val graphQLSchema = SchemaParser().parse(schema)
4646
val generator = GraphQLClientGenerator(graphQLSchema, config)

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGenerator.kt

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import graphql.language.ObjectTypeDefinition
3232
import graphql.language.OperationDefinition
3333
import graphql.parser.Parser
3434
import graphql.schema.idl.TypeDefinitionRegistry
35+
import kotlinx.serialization.Serializable
3536
import java.io.File
3637

3738
private const val CORE_TYPES_PACKAGE = "com.expediagroup.graphql.client.types"
@@ -45,14 +46,23 @@ class GraphQLClientGenerator(
4546
) {
4647
private val documentParser: Parser = Parser()
4748
private val typeAliases: MutableMap<String, TypeAliasSpec> = mutableMapOf()
49+
private val sharedTypes: MutableMap<ClassName, List<TypeSpec>> = mutableMapOf()
4850

4951
/**
5052
* Generate GraphQL clients for the specified queries.
5153
*/
5254
fun generate(queries: List<File>): List<FileSpec> {
5355
val result = mutableListOf<FileSpec>()
5456
for (query in queries) {
55-
result.add(generate(query))
57+
result.addAll(generate(query))
58+
}
59+
60+
for ((className, typeSpecs) in sharedTypes) {
61+
val fileSpec = FileSpec.builder(className.packageName, className.simpleName)
62+
for (type in typeSpecs) {
63+
fileSpec.addType(type)
64+
}
65+
result.add(fileSpec.build())
5666
}
5767
if (typeAliases.isNotEmpty()) {
5868
val typeAliasSpec = FileSpec.builder(packageName = config.packageName, fileName = "GraphQLTypeAliases")
@@ -67,7 +77,7 @@ class GraphQLClientGenerator(
6777
/**
6878
* Generate GraphQL client wrapper class and data classes that match the specified query.
6979
*/
70-
internal fun generate(queryFile: File): FileSpec {
80+
internal fun generate(queryFile: File): List<FileSpec> {
7181
val queryConst = queryFile.readText().trim()
7282
val queryDocument = documentParser.parseDocument(queryConst)
7383

@@ -76,8 +86,8 @@ class GraphQLClientGenerator(
7686
throw MultipleOperationsInFileException
7787
}
7888

79-
val fileSpec = FileSpec.builder(packageName = config.packageName, fileName = queryFile.nameWithoutExtension.capitalize())
80-
89+
val fileSpecs = mutableListOf<FileSpec>()
90+
val operationFileSpec = FileSpec.builder(packageName = config.packageName, fileName = queryFile.nameWithoutExtension.capitalize())
8191
operationDefinitions.forEach { operationDefinition ->
8292
val operationTypeName = operationDefinition.name?.capitalize() ?: queryFile.nameWithoutExtension.capitalize()
8393
val context = GraphQLClientGeneratorContext(
@@ -86,14 +96,15 @@ class GraphQLClientGenerator(
8696
rootType = operationTypeName,
8797
queryDocument = queryDocument,
8898
allowDeprecated = config.allowDeprecated,
89-
customScalarMap = config.customScalarMap
99+
customScalarMap = config.customScalarMap,
100+
serializer = config.serializer
90101
)
91102
val queryConstName = operationTypeName.toUpperUnderscore()
92103
val queryConstProp = PropertySpec.builder(queryConstName, STRING)
93104
.addModifiers(KModifier.CONST)
94105
.initializer("%S", queryConst)
95106
.build()
96-
fileSpec.addProperty(queryConstProp)
107+
operationFileSpec.addProperty(queryConstProp)
97108

98109
val rootType = findRootType(operationDefinition)
99110
val graphQLResponseTypeSpec = generateGraphQLObjectTypeSpec(context, rootType, operationDefinition.selectionSet, "Result")
@@ -106,6 +117,9 @@ class GraphQLClientGenerator(
106117
.addSuperinterface(ClassName(CORE_TYPES_PACKAGE, "GraphQLClientRequest").parameterizedBy(kotlinResultTypeName))
107118
.addProperty(queryProperty)
108119

120+
if (config.serializer == GraphQLSerializer.KOTLINX) {
121+
operationTypeSpec.addAnnotation(Serializable::class)
122+
}
109123
if (operationDefinition.name != null) {
110124
val operationNameProperty = PropertySpec.builder("operationName", STRING, KModifier.OVERRIDE)
111125
.initializer("%S", operationDefinition.name)
@@ -137,15 +151,36 @@ class GraphQLClientGenerator(
137151
.addStatement("return %T::class", kotlinResultTypeName)
138152
.build()
139153
)
140-
141-
context.typeSpecs.forEach {
142-
operationTypeSpec.addType(it.value)
154+
operationTypeSpec.addType(graphQLResponseTypeSpec)
155+
156+
val polymorphicTypes = mutableListOf<ClassName>()
157+
for ((superClassName, implementations) in context.polymorphicTypes) {
158+
polymorphicTypes.add(superClassName)
159+
val polymorphicTypeSpec = FileSpec.builder(superClassName.packageName, superClassName.simpleName)
160+
for (implementation in implementations) {
161+
polymorphicTypes.add(implementation)
162+
context.typeSpecs[implementation]?.let { typeSpec ->
163+
polymorphicTypeSpec.addType(typeSpec)
164+
}
165+
}
166+
fileSpecs.add(polymorphicTypeSpec.build())
167+
}
168+
context.typeSpecs.minus(polymorphicTypes).forEach { (className, typeSpec) ->
169+
val outputTypeFileSpec = FileSpec.builder(className.packageName, className.simpleName)
170+
.addType(typeSpec)
171+
.build()
172+
fileSpecs.add(outputTypeFileSpec)
143173
}
144-
fileSpec.addType(operationTypeSpec.build())
174+
operationFileSpec.addType(operationTypeSpec.build())
175+
fileSpecs.add(operationFileSpec.build())
145176

177+
// shared types
178+
sharedTypes.putAll(context.enumClassToTypeSpecs.mapValues { listOf(it.value) })
179+
sharedTypes.putAll(context.inputClassToTypeSpecs.mapValues { listOf(it.value) })
180+
sharedTypes.putAll(context.scalarsClassToTypeSpec)
146181
typeAliases.putAll(context.typeAliases)
147182
}
148-
return fileSpec.build()
183+
return fileSpecs
149184
}
150185

151186
private fun findRootType(operationDefinition: OperationDefinition): ObjectTypeDefinition {

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGeneratorConfig.kt

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ data class GraphQLClientGeneratorConfig(
2626
val allowDeprecated: Boolean = false,
2727
/** Custom scalar type to converter mapping. */
2828
val customScalarMap: Map<String, GraphQLScalar> = emptyMap(),
29-
/** Supported client type to be generated. */
30-
val clientType: GraphQLClientType = GraphQLClientType.DEFAULT
29+
/** Type of JSON serializer to be used. */
30+
val serializer: GraphQLSerializer = GraphQLSerializer.JACKSON
3131
)
3232

3333
/**
@@ -43,13 +43,9 @@ data class GraphQLScalar(
4343
)
4444

4545
/**
46-
* Type of GraphQL HTTP client to generate.
46+
* Type of JSON serializer that will be used to generate the data classes.
4747
*/
48-
enum class GraphQLClientType {
49-
/** Generic GraphQL client. */
50-
DEFAULT,
51-
/** Ktor based GraphQL client. */
52-
KTOR,
53-
/** Spring WebClient based GraphQL client. */
54-
WEBCLIENT
48+
enum class GraphQLSerializer {
49+
KOTLINX,
50+
JACKSON
5551
}

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGeneratorContext.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,19 @@ data class GraphQLClientGeneratorContext(
3333
val rootType: String,
3434
val queryDocument: Document,
3535
val allowDeprecated: Boolean = false,
36-
val customScalarMap: Map<String, GraphQLScalar> = mapOf()
36+
val customScalarMap: Map<String, GraphQLScalar> = mapOf(),
37+
val serializer: GraphQLSerializer = GraphQLSerializer.JACKSON
3738
) {
38-
val classNameCache: MutableMap<String, MutableList<ClassName>> = mutableMapOf()
39-
val typeSpecs: MutableMap<String, TypeSpec> = mutableMapOf()
39+
val typeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
40+
val polymorphicTypes: MutableMap<ClassName, MutableList<ClassName>> = mutableMapOf()
41+
42+
// shared types
43+
val enumClassToTypeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
44+
val inputClassToTypeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
45+
val scalarsClassToTypeSpec: MutableMap<ClassName, MutableList<TypeSpec>> = mutableMapOf()
4046
val typeAliases: MutableMap<String, TypeAliasSpec> = mutableMapOf()
4147

48+
// class name and type selection caches
49+
val classNameCache: MutableMap<String, MutableList<ClassName>> = mutableMapOf()
4250
val typeToSelectionSetMap: MutableMap<String, Set<String>> = mutableMapOf()
4351
}

0 commit comments

Comments
 (0)