Skip to content

Commit d3fc878

Browse files
committed
Generates data fetcher interface for query types
1 parent 2603477 commit d3fc878

File tree

4 files changed

+200
-1
lines changed

4 files changed

+200
-1
lines changed

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,13 @@ class CodeGen(private val config: CodeGenConfig) {
9898
codeGenResult.kotlinInputTypes.forEach { it.writeTo(config.outputDir) }
9999
codeGenResult.kotlinInterfaces.forEach { it.writeTo(config.outputDir) }
100100
codeGenResult.kotlinEnumTypes.forEach { it.writeTo(config.outputDir) }
101-
codeGenResult.kotlinDataFetchers.forEach { it.writeTo(config.examplesOutputDir) }
101+
codeGenResult.kotlinDataFetchers.forEach {
102+
if (config.generateDataFetcherInterfaces) {
103+
it.writeTo(config.outputDir)
104+
} else {
105+
it.writeTo(config.examplesOutputDir)
106+
}
107+
}
102108
codeGenResult.kotlinConstants.forEach { it.writeTo(config.outputDir) }
103109
codeGenResult.kotlinClientTypes.forEach { it.writeTo(config.outputDir) }
104110
codeGenResult.docFiles.forEach { it.writeTo(config.generatedDocsFolder) }
@@ -387,12 +393,23 @@ class CodeGen(private val config: CodeGenConfig) {
387393

388394
val constantsClass = KotlinConstantsGenerator(config, document).generate()
389395

396+
val dataFetchers = if (config.generateDataFetcherInterfaces) {
397+
definitions.asSequence()
398+
.filterIsInstance<ObjectTypeDefinition>()
399+
.filter { it.name == "Query" }
400+
.map { KotlinDataFetcherGenerator(config, document).generate(it) }
401+
.fold(CodeGenResult()) { result, next -> result.merge(next) }
402+
} else {
403+
CodeGenResult()
404+
}
405+
390406
datatypesResult
391407
.merge(inputTypes)
392408
.merge(interfacesResult)
393409
.merge(unionResult)
394410
.merge(enumsResult)
395411
.merge(constantsClass)
412+
.merge(dataFetchers)
396413
}
397414

398415
val clientTypes = if (config.generateKotlinClosureProjections) {
@@ -505,6 +522,7 @@ class CodeGenConfig(
505522
var generateInterfaces: Boolean = false,
506523
var generateKotlinNullableClasses: Boolean = false,
507524
var generateKotlinClosureProjections: Boolean = false,
525+
var generateDataFetcherInterfaces: Boolean = false,
508526
var typeMapping: Map<String, String> = emptyMap(),
509527
var includeQueries: Set<String> = emptySet(),
510528
var includeMutations: Set<String> = emptySet(),
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.netflix.graphql.dgs.codegen.generators.kotlin
2+
3+
import com.netflix.graphql.dgs.DgsComponent
4+
import com.netflix.graphql.dgs.DgsData
5+
import com.netflix.graphql.dgs.InputArgument
6+
import com.netflix.graphql.dgs.codegen.CodeGenConfig
7+
import com.netflix.graphql.dgs.codegen.CodeGenResult
8+
import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized
9+
import com.squareup.kotlinpoet.AnnotationSpec
10+
import com.squareup.kotlinpoet.FileSpec
11+
import com.squareup.kotlinpoet.FunSpec
12+
import com.squareup.kotlinpoet.KModifier
13+
import com.squareup.kotlinpoet.ParameterSpec
14+
import com.squareup.kotlinpoet.TypeSpec
15+
import graphql.language.Document
16+
import graphql.language.FieldDefinition
17+
import graphql.language.ObjectTypeDefinition
18+
import graphql.schema.DataFetchingEnvironment
19+
20+
class KotlinDataFetcherGenerator(private val config: CodeGenConfig, private val document: Document) {
21+
22+
private val packageName = config.packageNameDatafetchers
23+
private val typeUtils = KotlinTypeUtils(config.packageNameTypes, config, document)
24+
private val dsgConstantsPackageName = config.packageName
25+
26+
fun generate(query: ObjectTypeDefinition): CodeGenResult =
27+
query.fieldDefinitions
28+
.map { generateField(it) }
29+
.fold(CodeGenResult()) { left, right -> left.merge(right) }
30+
31+
private fun generateField(field: FieldDefinition): CodeGenResult {
32+
val fieldName = field.name.capitalized()
33+
val className = fieldName + "Datafetcher"
34+
35+
val returnType = typeUtils.findReturnType(field.type)
36+
37+
val dsgDataAnnotation = AnnotationSpec.builder(DgsData::class)
38+
.addMember("parentType = DgsConstants.QUERY.TYPE_NAME")
39+
.addMember("field = DgsConstants.QUERY.$fieldName")
40+
.build()
41+
42+
val methodSpec = FunSpec.builder("get$fieldName")
43+
.addAnnotation(dsgDataAnnotation)
44+
.addModifiers(KModifier.ABSTRACT)
45+
.addInputArguments(field)
46+
.addParameter("dataFetchingEnvironment", DataFetchingEnvironment::class)
47+
.returns(returnType)
48+
.build()
49+
50+
val interfaceBuilder = TypeSpec.interfaceBuilder(className)
51+
.addAnnotation(DgsComponent::class)
52+
.addFunction(methodSpec)
53+
.build()
54+
55+
val fileSpec = FileSpec.builder(packageName, interfaceBuilder.name!!)
56+
.addType(interfaceBuilder)
57+
.addImport(dsgConstantsPackageName, "DgsConstants")
58+
.build()
59+
60+
return CodeGenResult(kotlinDataFetchers = listOf(fileSpec))
61+
}
62+
63+
private fun FunSpec.Builder.addInputArguments(field: FieldDefinition): FunSpec.Builder = apply {
64+
field.inputValueDefinitions.forEach { input ->
65+
val inputAnnotation = AnnotationSpec.builder(InputArgument::class)
66+
.addMember("\"${input.name}\"")
67+
.build()
68+
val inputType = ParameterSpec.builder(input.name, typeUtils.findReturnType(input.type))
69+
.addAnnotation(inputAnnotation)
70+
.build()
71+
addParameter(inputType)
72+
}
73+
}
74+
}

graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt

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

2121
import com.squareup.kotlinpoet.*
2222
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
23+
import graphql.schema.DataFetchingEnvironment
2324
import org.assertj.core.api.Assertions.assertThat
2425
import org.assertj.core.api.Assertions.assertThatThrownBy
2526
import org.assertj.core.data.Index
@@ -35,6 +36,10 @@ import java.util.stream.Stream.of
3536

3637
class KotlinCodeGenTest {
3738

39+
val basePackageName = "com.netflix.graphql.dgs.codegen.tests.generated"
40+
val typesPackageName = "$basePackageName.types"
41+
val datafetchersPackageName = "$basePackageName.datafetchers"
42+
3843
@Test
3944
fun generateDataClassWithStringProperties() {
4045
val schema = """
@@ -68,6 +73,104 @@ class KotlinCodeGenTest {
6873
assertCompilesKotlin(dataTypes)
6974
}
7075

76+
@Test
77+
fun generateDataFetcherInterfaceWithFunction() {
78+
val schema = """
79+
type Query {
80+
people: [Person]
81+
}
82+
83+
type Person {
84+
firstname: String
85+
lastname: String
86+
}
87+
""".trimIndent()
88+
89+
val dataFetchers = CodeGen(
90+
CodeGenConfig(
91+
schemas = setOf(schema),
92+
packageName = basePackageName,
93+
language = Language.KOTLIN,
94+
generateDataFetcherInterfaces = true
95+
)
96+
).generate().kotlinDataFetchers
97+
98+
assertThat(dataFetchers.size).isEqualTo(1)
99+
assertThat(dataFetchers[0].name).isEqualTo("PeopleDatafetcher")
100+
assertThat(dataFetchers[0].packageName).isEqualTo(datafetchersPackageName)
101+
val type = dataFetchers[0].members[0] as TypeSpec
102+
103+
assertThat(type.kind).isEqualTo(TypeSpec.Kind.INTERFACE)
104+
assertThat(type.annotations).hasSize(1).first().satisfies({
105+
assertThat(it.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.DgsComponent")
106+
})
107+
assertThat(type.funSpecs).hasSize(1)
108+
val fn = type.funSpecs.single()
109+
assertThat(fn.name).isEqualTo("getPeople")
110+
val returnType = fn.returnType as ParameterizedTypeName
111+
assertThat(fn.returnType)
112+
assertThat(returnType.rawType.canonicalName).isEqualTo(List::class.qualifiedName)
113+
assertThat(returnType.typeArguments).hasSize(1)
114+
val arg0 = returnType.typeArguments.single() as ClassName
115+
assertThat(arg0.canonicalName).isEqualTo("$typesPackageName.Person")
116+
assertThat(fn.parameters).hasSize(1)
117+
val param0 = fn.parameters.single()
118+
assertThat(param0.name).isEqualTo("dataFetchingEnvironment")
119+
assertThat((param0.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName)
120+
assertThat(fn.annotations).hasSize(1).first().satisfies({ annotation ->
121+
assertThat(annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.DgsData")
122+
assertThat(annotation.members).satisfiesExactly(
123+
{ member -> assertThat(member.toString()).isEqualTo("parentType = DgsConstants.QUERY.TYPE_NAME") },
124+
{ member -> assertThat(member.toString()).isEqualTo("field = DgsConstants.QUERY.People") }
125+
)
126+
})
127+
}
128+
129+
@Test
130+
fun generateDataFetcherInterfaceWithArgument() {
131+
val schema = """
132+
type Query {
133+
person(name: String): Person
134+
}
135+
136+
type Person {
137+
firstname: String
138+
lastname: String
139+
}
140+
""".trimIndent()
141+
142+
val dataFetchers = CodeGen(
143+
CodeGenConfig(
144+
schemas = setOf(schema),
145+
packageName = basePackageName,
146+
language = Language.KOTLIN,
147+
generateDataFetcherInterfaces = true
148+
)
149+
).generate().kotlinDataFetchers
150+
151+
assertThat(dataFetchers.size).isEqualTo(1)
152+
assertThat(dataFetchers[0].name).isEqualTo("PersonDatafetcher")
153+
assertThat(dataFetchers[0].packageName).isEqualTo(datafetchersPackageName)
154+
val type = dataFetchers[0].members[0] as TypeSpec
155+
156+
assertThat(type.kind).isEqualTo(TypeSpec.Kind.INTERFACE)
157+
assertThat(type.funSpecs).hasSize(1)
158+
val fn = type.funSpecs.single()
159+
assertThat(fn.name).isEqualTo("getPerson")
160+
assertThat((fn.returnType as ClassName).canonicalName).isEqualTo("$typesPackageName.Person")
161+
assertThat(fn.parameters).hasSize(2)
162+
val arg0 = fn.parameters[0]
163+
assertThat(arg0.name).isEqualTo("name")
164+
assertThat((arg0.type as ClassName).canonicalName).isEqualTo(String::class.qualifiedName)
165+
assertThat(arg0.annotations).hasSize(1)
166+
val arg0Annotation = arg0.annotations[0]
167+
assertThat(arg0Annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.InputArgument")
168+
assertThat(arg0Annotation.members.single().toString()).isEqualTo("\"name\"")
169+
val arg1 = fn.parameters[1]
170+
assertThat(arg1.name).isEqualTo("dataFetchingEnvironment")
171+
assertThat((arg1.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName)
172+
}
173+
71174
@Test
72175
fun generateDataClassWithNullablePrimitive() {
73176
val schema = """

graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ open class GenerateJavaTask @Inject constructor(
8383
@Input
8484
var generateKotlinClosureProjections = false
8585

86+
@Input
87+
var generateDataFetcherInterfaces = false
88+
8689
@Input
8790
var generateDataTypes = true
8891

@@ -194,6 +197,7 @@ open class GenerateJavaTask @Inject constructor(
194197
generateClientApiv2 = generateClientv2,
195198
generateKotlinNullableClasses = generateKotlinNullableClasses,
196199
generateKotlinClosureProjections = generateKotlinClosureProjections,
200+
generateDataFetcherInterfaces = generateDataFetcherInterfaces,
197201
generateInterfaces = generateInterfaces,
198202
generateInterfaceSetters = generateInterfaceSetters,
199203
generateInterfaceMethodsForInterfaceFields = generateInterfaceMethodsForInterfaceFields,

0 commit comments

Comments
 (0)