Skip to content

Commit 2742416

Browse files
authored
Feature/kotlin codegen (#359)
New style of kotlin code-gen * Generates data classes that match GQL schema nullability. These allow for partial contstruction for use as return values in a query that asks for a partial object. If any field (nullable or not) is used but was not requested, it will throw an exception. These classes use suppliers as field initializers to defer evaluation until use. * Adds projections using kotlin closures. This very nearly matches the GQL query syntax as compiled kotlin. * All new styles are behind to options: `generateKotlinNullableClasses` and `generateKotlinClosureProjections` * Uses new style of file based testing to preview generated code
1 parent dfa49ab commit 2742416

File tree

377 files changed

+9713
-47
lines changed

Some content is hidden

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

377 files changed

+9713
-47
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
*
3+
* Copyright 2020 Netflix, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package com.netflix.graphql.dgs.client.codegen
20+
21+
@DslMarker
22+
annotation class QueryProjectionMarker
23+
24+
@QueryProjectionMarker
25+
abstract class GraphQLProjection(defaultFields: Set<String> = setOf("__typename")) : GraphQLInput() {
26+
27+
private val builder = StringBuilder("{ ${defaultFields.joinToString(" ")} ")
28+
29+
protected fun field(field: String) {
30+
builder.append("$field ")
31+
}
32+
33+
protected fun <T : GraphQLProjection> project(field: String, projection: T, projectionFields: T.() -> T) {
34+
builder.append("$field ")
35+
projectionFields.invoke(projection)
36+
builder.append(projection.asQuery())
37+
}
38+
39+
fun asQuery() = "$builder}"
40+
}
41+
42+
abstract class GraphQLInput {
43+
44+
companion object {
45+
46+
private val inputSerializer = InputValueSerializer()
47+
48+
protected fun inputToString(value: Any?): String {
49+
// TODO escape newlines in InputValueSerializer
50+
return inputSerializer.serialize(value).replace("\n", "\\n")
51+
}
52+
53+
val defaults: ThreadLocal<MutableSet<String>> = ThreadLocal.withInitial { mutableSetOf() }
54+
55+
@JvmStatic
56+
protected fun <T> default(arg: String): T? {
57+
defaults.get().add(arg)
58+
return null
59+
}
60+
}
61+
62+
private val _defaults = defaults.get()
63+
64+
init {
65+
defaults.set(mutableSetOf())
66+
}
67+
68+
protected fun formatArgs(vararg args: Pair<String, Any?>): String {
69+
return args
70+
.filter { (k, _) -> !_defaults.contains(k) }
71+
.joinToString(", ") { (k, v) -> "$k: ${inputToString(v)}" }
72+
}
73+
}

graphql-dgs-codegen-core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies {
2929
implementation(project(":graphql-dgs-codegen-client-core"))
3030
implementation 'com.graphql-java:graphql-java'
3131
implementation 'com.fasterxml.jackson.core:jackson-annotations'
32+
implementation 'com.fasterxml.jackson.core:jackson-databind'
3233
implementation 'org.slf4j:slf4j-api'
3334

3435
implementation 'com.squareup:javapoet:1.13.+'

graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt

Lines changed: 126 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ package com.netflix.graphql.dgs.codegen
2020

2121
import com.netflix.graphql.dgs.codegen.generators.java.*
2222
import com.netflix.graphql.dgs.codegen.generators.kotlin.*
23+
import com.netflix.graphql.dgs.codegen.generators.kotlin2.generateKotlin2ClientTypes
24+
import com.netflix.graphql.dgs.codegen.generators.kotlin2.generateKotlin2DataTypes
25+
import com.netflix.graphql.dgs.codegen.generators.kotlin2.generateKotlin2EnumTypes
26+
import com.netflix.graphql.dgs.codegen.generators.kotlin2.generateKotlin2InputTypes
27+
import com.netflix.graphql.dgs.codegen.generators.kotlin2.generateKotlin2Interfaces
2328
import com.netflix.graphql.dgs.codegen.generators.shared.SchemaExtensionsUtils.findEnumExtensions
2429
import com.netflix.graphql.dgs.codegen.generators.shared.SchemaExtensionsUtils.findInputExtensions
2530
import com.netflix.graphql.dgs.codegen.generators.shared.SchemaExtensionsUtils.findInterfaceExtensions
@@ -32,6 +37,7 @@ import graphql.language.*
3237
import graphql.parser.MultiSourceReader
3338
import graphql.parser.Parser
3439
import graphql.parser.ParserOptions
40+
import graphql.schema.idl.TypeUtil
3541
import java.io.File
3642
import java.nio.file.Path
3743
import java.nio.file.Paths
@@ -73,10 +79,12 @@ class CodeGen(private val config: CodeGenConfig) {
7379
}
7480
codeGenResult.javaConstants.forEach { it.writeTo(config.outputDir) }
7581
codeGenResult.kotlinDataTypes.forEach { it.writeTo(config.outputDir) }
82+
codeGenResult.kotlinInputTypes.forEach { it.writeTo(config.outputDir) }
7683
codeGenResult.kotlinInterfaces.forEach { it.writeTo(config.outputDir) }
7784
codeGenResult.kotlinEnumTypes.forEach { it.writeTo(config.outputDir) }
7885
codeGenResult.kotlinDataFetchers.forEach { it.writeTo(config.examplesOutputDir) }
7986
codeGenResult.kotlinConstants.forEach { it.writeTo(config.outputDir) }
87+
codeGenResult.kotlinClientTypes.forEach { it.writeTo(config.outputDir) }
8088
}
8189

8290
return codeGenResult
@@ -105,9 +113,50 @@ class CodeGen(private val config: CodeGenConfig) {
105113
readerBuilder.string(schema, null)
106114
}
107115

108-
return readerBuilder.build().use { reader ->
116+
val document = readerBuilder.build().use { reader ->
109117
parser.parseDocument(reader, options)
110118
}
119+
120+
return document.transform {
121+
122+
// for kotlin2, add implicit types like PageInfo to the schema so classes are generated
123+
if (config.generateKotlinNullableClasses || config.generateKotlinClosureProjections) {
124+
val objectTypeDefs = document.getDefinitionsOfType(ObjectTypeDefinition::class.java)
125+
if (!objectTypeDefs.any { def -> def.name == "PageInfo" } &&
126+
objectTypeDefs.any { def -> def.fieldDefinitions.any { field -> TypeUtil.unwrapAll(field.type).name == "PageInfo" } }
127+
) {
128+
it.definition(
129+
ObjectTypeDefinition.newObjectTypeDefinition()
130+
.name("PageInfo")
131+
.fieldDefinition(
132+
FieldDefinition.newFieldDefinition()
133+
.name("hasNextPage")
134+
.type(NonNullType(TypeName("Boolean")))
135+
.build()
136+
)
137+
.fieldDefinition(
138+
FieldDefinition.newFieldDefinition()
139+
.name("hasPreviousPage")
140+
.type(NonNullType(TypeName("Boolean")))
141+
.build()
142+
)
143+
.fieldDefinition(
144+
FieldDefinition.newFieldDefinition()
145+
.name("startCursor")
146+
.type(TypeName("String"))
147+
.build()
148+
)
149+
.fieldDefinition(
150+
FieldDefinition.newFieldDefinition()
151+
.name("endCursor")
152+
.type(TypeName("String"))
153+
.build()
154+
)
155+
.build()
156+
)
157+
}
158+
}
159+
}
111160
}
112161

113162
private fun generateJava(): CodeGenResult {
@@ -239,37 +288,72 @@ class CodeGen(private val config: CodeGenConfig) {
239288
private fun generateKotlin(): CodeGenResult {
240289
val definitions = document.definitions
241290

242-
val datatypesResult = generateKotlinDataTypes(definitions)
243-
val inputTypes = generateKotlinInputTypes(definitions)
244-
val interfacesResult = generateKotlinInterfaceTypes(definitions)
291+
val requiredTypeCollector = RequiredTypeCollector(
292+
document = document,
293+
queries = config.includeQueries,
294+
mutations = config.includeMutations,
295+
subscriptions = config.includeSubscriptions,
296+
)
297+
val requiredTypes = requiredTypeCollector.requiredTypes
298+
299+
val dataTypes = if (config.generateKotlinNullableClasses) {
300+
301+
CodeGenResult(
302+
kotlinDataTypes = generateKotlin2DataTypes(config, document, requiredTypes),
303+
kotlinInputTypes = generateKotlin2InputTypes(config, document, requiredTypes),
304+
kotlinInterfaces = generateKotlin2Interfaces(config, document),
305+
kotlinEnumTypes = generateKotlin2EnumTypes(config, document, requiredTypes),
306+
kotlinConstants = KotlinConstantsGenerator(config, document).generate().kotlinConstants,
307+
)
308+
} else {
309+
310+
val datatypesResult = generateKotlinDataTypes(definitions)
311+
val inputTypes = generateKotlinInputTypes(definitions)
312+
val interfacesResult = generateKotlinInterfaceTypes(definitions)
313+
314+
val unionResult = definitions.asSequence()
315+
.filterIsInstance<UnionTypeDefinition>()
316+
.excludeSchemaTypeExtension()
317+
.map {
318+
val extensions = findUnionExtensions(it.name, definitions)
319+
KotlinUnionTypeGenerator(config).generate(it, extensions)
320+
}
321+
.fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) }
245322

246-
val unionResult = definitions.asSequence()
247-
.filterIsInstance<UnionTypeDefinition>()
248-
.excludeSchemaTypeExtension()
249-
.map {
250-
val extensions = findUnionExtensions(it.name, definitions)
251-
KotlinUnionTypeGenerator(config).generate(it, extensions)
252-
}
253-
.fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) }
323+
val enumsResult = definitions.asSequence()
324+
.filterIsInstance<EnumTypeDefinition>()
325+
.excludeSchemaTypeExtension()
326+
.filter { config.generateDataTypes || it.name in requiredTypeCollector.requiredTypes }
327+
.map {
328+
val extensions = findEnumExtensions(it.name, definitions)
329+
KotlinEnumTypeGenerator(config).generate(it, extensions)
330+
}
331+
.fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) }
254332

255-
val enumsResult = definitions.asSequence()
256-
.filterIsInstance<EnumTypeDefinition>()
257-
.excludeSchemaTypeExtension()
258-
.filter { config.generateDataTypes || it.name in requiredTypeCollector.requiredTypes }
259-
.map {
260-
val extensions = findEnumExtensions(it.name, definitions)
261-
KotlinEnumTypeGenerator(config).generate(it, extensions)
262-
}
263-
.fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) }
333+
val constantsClass = KotlinConstantsGenerator(config, document).generate()
264334

265-
val constantsClass = KotlinConstantsGenerator(config, document).generate()
335+
datatypesResult
336+
.merge(inputTypes)
337+
.merge(interfacesResult)
338+
.merge(unionResult)
339+
.merge(enumsResult)
340+
.merge(constantsClass)
341+
}
266342

267-
val client = generateJavaClientApi(definitions)
268-
val entitiesClient = generateJavaClientEntitiesApi(definitions)
269-
val entitiesRepresentationsTypes = generateKotlinClientEntitiesRepresentations(definitions)
343+
val clientTypes = if (config.generateKotlinClosureProjections) {
344+
CodeGenResult(
345+
kotlinClientTypes = generateKotlin2ClientTypes(config, document),
346+
)
347+
} else {
348+
349+
val client = generateJavaClientApi(definitions)
350+
val entitiesClient = generateJavaClientEntitiesApi(definitions)
351+
val entitiesRepresentationsTypes = generateKotlinClientEntitiesRepresentations(definitions)
270352

271-
return datatypesResult.merge(inputTypes).merge(interfacesResult).merge(unionResult).merge(enumsResult)
272-
.merge(client).merge(entitiesClient).merge(entitiesRepresentationsTypes).merge(constantsClass)
353+
client.merge(entitiesClient).merge(entitiesRepresentationsTypes)
354+
}
355+
356+
return dataTypes.merge(clientTypes)
273357
}
274358

275359
private fun generateKotlinClientEntitiesRepresentations(definitions: Collection<Definition<*>>): CodeGenResult {
@@ -338,6 +422,8 @@ data class CodeGenConfig(
338422
val generateBoxedTypes: Boolean = false,
339423
val generateClientApi: Boolean = false,
340424
val generateInterfaces: Boolean = false,
425+
val generateKotlinNullableClasses: Boolean = false,
426+
val generateKotlinClosureProjections: Boolean = false,
341427
val typeMapping: Map<String, String> = emptyMap(),
342428
val includeQueries: Set<String> = emptySet(),
343429
val includeMutations: Set<String> = emptySet(),
@@ -382,7 +468,7 @@ data class CodeGenConfig(
382468

383469
enum class Language {
384470
JAVA,
385-
KOTLIN
471+
KOTLIN,
386472
}
387473

388474
data class CodeGenResult(
@@ -394,10 +480,12 @@ data class CodeGenResult(
394480
val clientProjections: List<JavaFile> = listOf(),
395481
val javaConstants: List<JavaFile> = listOf(),
396482
val kotlinDataTypes: List<FileSpec> = listOf(),
483+
val kotlinInputTypes: List<FileSpec> = listOf(),
397484
val kotlinInterfaces: List<FileSpec> = listOf(),
398485
val kotlinEnumTypes: List<FileSpec> = listOf(),
399486
val kotlinDataFetchers: List<FileSpec> = listOf(),
400-
val kotlinConstants: List<FileSpec> = emptyList()
487+
val kotlinConstants: List<FileSpec> = listOf(),
488+
val kotlinClientTypes: List<FileSpec> = listOf(),
401489
) {
402490
fun merge(current: CodeGenResult): CodeGenResult {
403491
val javaDataTypes = this.javaDataTypes.plus(current.javaDataTypes)
@@ -408,10 +496,12 @@ data class CodeGenResult(
408496
val clientProjections = this.clientProjections.plus(current.clientProjections)
409497
val javaConstants = this.javaConstants.plus(current.javaConstants)
410498
val kotlinDataTypes = this.kotlinDataTypes.plus(current.kotlinDataTypes)
499+
val kotlinInputTypes = this.kotlinInputTypes.plus(current.kotlinInputTypes)
411500
val kotlinInterfaces = this.kotlinInterfaces.plus(current.kotlinInterfaces)
412501
val kotlinEnumTypes = this.kotlinEnumTypes.plus(current.kotlinEnumTypes)
413502
val kotlinDataFetchers = this.kotlinDataFetchers.plus(current.kotlinDataFetchers)
414503
val kotlinConstants = this.kotlinConstants.plus(current.kotlinConstants)
504+
val kotlinClientTypes = this.kotlinClientTypes.plus(current.kotlinClientTypes)
415505

416506
return CodeGenResult(
417507
javaDataTypes = javaDataTypes,
@@ -422,32 +512,32 @@ data class CodeGenResult(
422512
clientProjections = clientProjections,
423513
javaConstants = javaConstants,
424514
kotlinDataTypes = kotlinDataTypes,
515+
kotlinInputTypes = kotlinInputTypes,
425516
kotlinInterfaces = kotlinInterfaces,
426517
kotlinEnumTypes = kotlinEnumTypes,
427518
kotlinDataFetchers = kotlinDataFetchers,
428-
kotlinConstants = kotlinConstants
519+
kotlinConstants = kotlinConstants,
520+
kotlinClientTypes = kotlinClientTypes,
429521
)
430522
}
431523

432524
fun javaSources(): List<JavaFile> {
433525
return javaDataTypes
434-
.asSequence()
435526
.plus(javaInterfaces)
436527
.plus(javaEnumTypes)
437528
.plus(javaDataFetchers)
438529
.plus(javaQueryTypes)
439530
.plus(clientProjections)
440531
.plus(javaConstants)
441-
.toList()
442532
}
443533

444534
fun kotlinSources(): List<FileSpec> {
445535
return kotlinDataTypes
446-
.asSequence()
536+
.plus(kotlinInputTypes)
447537
.plus(kotlinInterfaces)
448538
.plus(kotlinEnumTypes)
449539
.plus(kotlinConstants)
450-
.toList()
540+
.plus(kotlinClientTypes)
451541
}
452542
}
453543

@@ -482,21 +572,10 @@ fun List<FieldDefinition>.filterIncludedInConfig(definitionName: String, config:
482572
}
483573
}
484574

485-
fun ObjectTypeDefinition.shouldSkip(config: CodeGenConfig): Boolean = shouldSkip(this, config)
486-
487-
fun InputObjectTypeDefinition.shouldSkip(config: CodeGenConfig): Boolean = shouldSkip(this, config)
488-
489-
fun InterfaceTypeDefinition.shouldSkip(config: CodeGenConfig): Boolean = shouldSkip(this, config)
490-
491-
fun UnionTypeDefinition.shouldSkip(config: CodeGenConfig): Boolean = shouldSkip(this, config)
492-
493-
fun EnumTypeDefinition.shouldSkip(config: CodeGenConfig): Boolean = shouldSkip(this, config)
494-
495-
private fun <T : DirectivesContainer<*>> shouldSkip(
496-
typeDefinition: DirectivesContainer<T>,
575+
fun <T : DirectivesContainer<*>> DirectivesContainer<T>.shouldSkip(
497576
config: CodeGenConfig
498577
): Boolean {
499-
return typeDefinition.directives.any { it.name == "skipcodegen" } || config.typeMapping.containsKey((typeDefinition as NamedNode<*>).name)
578+
return directives.any { it.name == "skipcodegen" } || config.typeMapping.containsKey((this as NamedNode<*>).name)
500579
}
501580

502581
fun TypeDefinition<*>.fieldDefinitions(): List<FieldDefinition> {

0 commit comments

Comments
 (0)