From d3fc878f288937e9aec9eab74a6197bc16f7d0cd Mon Sep 17 00:00:00 2001 From: zaenk Date: Sat, 3 Apr 2021 15:40:35 +0200 Subject: [PATCH 1/5] Generates data fetcher interface for query types --- .../netflix/graphql/dgs/codegen/CodeGen.kt | 20 +++- .../kotlin/KotlinDataFetcherGenerator.kt | 74 +++++++++++++ .../graphql/dgs/codegen/KotlinCodeGenTest.kt | 103 ++++++++++++++++++ .../dgs/codegen/gradle/GenerateJavaTask.kt | 4 + 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt index 694a7fcc8..049dbbf43 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt @@ -98,7 +98,13 @@ class CodeGen(private val config: CodeGenConfig) { codeGenResult.kotlinInputTypes.forEach { it.writeTo(config.outputDir) } codeGenResult.kotlinInterfaces.forEach { it.writeTo(config.outputDir) } codeGenResult.kotlinEnumTypes.forEach { it.writeTo(config.outputDir) } - codeGenResult.kotlinDataFetchers.forEach { it.writeTo(config.examplesOutputDir) } + codeGenResult.kotlinDataFetchers.forEach { + if (config.generateDataFetcherInterfaces) { + it.writeTo(config.outputDir) + } else { + it.writeTo(config.examplesOutputDir) + } + } codeGenResult.kotlinConstants.forEach { it.writeTo(config.outputDir) } codeGenResult.kotlinClientTypes.forEach { it.writeTo(config.outputDir) } codeGenResult.docFiles.forEach { it.writeTo(config.generatedDocsFolder) } @@ -387,12 +393,23 @@ class CodeGen(private val config: CodeGenConfig) { val constantsClass = KotlinConstantsGenerator(config, document).generate() + val dataFetchers = if (config.generateDataFetcherInterfaces) { + definitions.asSequence() + .filterIsInstance() + .filter { it.name == "Query" } + .map { KotlinDataFetcherGenerator(config, document).generate(it) } + .fold(CodeGenResult()) { result, next -> result.merge(next) } + } else { + CodeGenResult() + } + datatypesResult .merge(inputTypes) .merge(interfacesResult) .merge(unionResult) .merge(enumsResult) .merge(constantsClass) + .merge(dataFetchers) } val clientTypes = if (config.generateKotlinClosureProjections) { @@ -505,6 +522,7 @@ class CodeGenConfig( var generateInterfaces: Boolean = false, var generateKotlinNullableClasses: Boolean = false, var generateKotlinClosureProjections: Boolean = false, + var generateDataFetcherInterfaces: Boolean = false, var typeMapping: Map = emptyMap(), var includeQueries: Set = emptySet(), var includeMutations: Set = emptySet(), diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt new file mode 100644 index 000000000..3691cedc6 --- /dev/null +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt @@ -0,0 +1,74 @@ +package com.netflix.graphql.dgs.codegen.generators.kotlin + +import com.netflix.graphql.dgs.DgsComponent +import com.netflix.graphql.dgs.DgsData +import com.netflix.graphql.dgs.InputArgument +import com.netflix.graphql.dgs.codegen.CodeGenConfig +import com.netflix.graphql.dgs.codegen.CodeGenResult +import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.TypeSpec +import graphql.language.Document +import graphql.language.FieldDefinition +import graphql.language.ObjectTypeDefinition +import graphql.schema.DataFetchingEnvironment + +class KotlinDataFetcherGenerator(private val config: CodeGenConfig, private val document: Document) { + + private val packageName = config.packageNameDatafetchers + private val typeUtils = KotlinTypeUtils(config.packageNameTypes, config, document) + private val dsgConstantsPackageName = config.packageName + + fun generate(query: ObjectTypeDefinition): CodeGenResult = + query.fieldDefinitions + .map { generateField(it) } + .fold(CodeGenResult()) { left, right -> left.merge(right) } + + private fun generateField(field: FieldDefinition): CodeGenResult { + val fieldName = field.name.capitalized() + val className = fieldName + "Datafetcher" + + val returnType = typeUtils.findReturnType(field.type) + + val dsgDataAnnotation = AnnotationSpec.builder(DgsData::class) + .addMember("parentType = DgsConstants.QUERY.TYPE_NAME") + .addMember("field = DgsConstants.QUERY.$fieldName") + .build() + + val methodSpec = FunSpec.builder("get$fieldName") + .addAnnotation(dsgDataAnnotation) + .addModifiers(KModifier.ABSTRACT) + .addInputArguments(field) + .addParameter("dataFetchingEnvironment", DataFetchingEnvironment::class) + .returns(returnType) + .build() + + val interfaceBuilder = TypeSpec.interfaceBuilder(className) + .addAnnotation(DgsComponent::class) + .addFunction(methodSpec) + .build() + + val fileSpec = FileSpec.builder(packageName, interfaceBuilder.name!!) + .addType(interfaceBuilder) + .addImport(dsgConstantsPackageName, "DgsConstants") + .build() + + return CodeGenResult(kotlinDataFetchers = listOf(fileSpec)) + } + + private fun FunSpec.Builder.addInputArguments(field: FieldDefinition): FunSpec.Builder = apply { + field.inputValueDefinitions.forEach { input -> + val inputAnnotation = AnnotationSpec.builder(InputArgument::class) + .addMember("\"${input.name}\"") + .build() + val inputType = ParameterSpec.builder(input.name, typeUtils.findReturnType(input.type)) + .addAnnotation(inputAnnotation) + .build() + addParameter(inputType) + } + } +} diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt index 0a16f70df..4b5aadf66 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt @@ -20,6 +20,7 @@ package com.netflix.graphql.dgs.codegen import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import graphql.schema.DataFetchingEnvironment import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.data.Index @@ -35,6 +36,10 @@ import java.util.stream.Stream.of class KotlinCodeGenTest { + val basePackageName = "com.netflix.graphql.dgs.codegen.tests.generated" + val typesPackageName = "$basePackageName.types" + val datafetchersPackageName = "$basePackageName.datafetchers" + @Test fun generateDataClassWithStringProperties() { val schema = """ @@ -68,6 +73,104 @@ class KotlinCodeGenTest { assertCompilesKotlin(dataTypes) } + @Test + fun generateDataFetcherInterfaceWithFunction() { + val schema = """ + type Query { + people: [Person] + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + val dataFetchers = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + language = Language.KOTLIN, + generateDataFetcherInterfaces = true + ) + ).generate().kotlinDataFetchers + + assertThat(dataFetchers.size).isEqualTo(1) + assertThat(dataFetchers[0].name).isEqualTo("PeopleDatafetcher") + assertThat(dataFetchers[0].packageName).isEqualTo(datafetchersPackageName) + val type = dataFetchers[0].members[0] as TypeSpec + + assertThat(type.kind).isEqualTo(TypeSpec.Kind.INTERFACE) + assertThat(type.annotations).hasSize(1).first().satisfies({ + assertThat(it.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.DgsComponent") + }) + assertThat(type.funSpecs).hasSize(1) + val fn = type.funSpecs.single() + assertThat(fn.name).isEqualTo("getPeople") + val returnType = fn.returnType as ParameterizedTypeName + assertThat(fn.returnType) + assertThat(returnType.rawType.canonicalName).isEqualTo(List::class.qualifiedName) + assertThat(returnType.typeArguments).hasSize(1) + val arg0 = returnType.typeArguments.single() as ClassName + assertThat(arg0.canonicalName).isEqualTo("$typesPackageName.Person") + assertThat(fn.parameters).hasSize(1) + val param0 = fn.parameters.single() + assertThat(param0.name).isEqualTo("dataFetchingEnvironment") + assertThat((param0.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) + assertThat(fn.annotations).hasSize(1).first().satisfies({ annotation -> + assertThat(annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.DgsData") + assertThat(annotation.members).satisfiesExactly( + { member -> assertThat(member.toString()).isEqualTo("parentType = DgsConstants.QUERY.TYPE_NAME") }, + { member -> assertThat(member.toString()).isEqualTo("field = DgsConstants.QUERY.People") } + ) + }) + } + + @Test + fun generateDataFetcherInterfaceWithArgument() { + val schema = """ + type Query { + person(name: String): Person + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + val dataFetchers = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + language = Language.KOTLIN, + generateDataFetcherInterfaces = true + ) + ).generate().kotlinDataFetchers + + assertThat(dataFetchers.size).isEqualTo(1) + assertThat(dataFetchers[0].name).isEqualTo("PersonDatafetcher") + assertThat(dataFetchers[0].packageName).isEqualTo(datafetchersPackageName) + val type = dataFetchers[0].members[0] as TypeSpec + + assertThat(type.kind).isEqualTo(TypeSpec.Kind.INTERFACE) + assertThat(type.funSpecs).hasSize(1) + val fn = type.funSpecs.single() + assertThat(fn.name).isEqualTo("getPerson") + assertThat((fn.returnType as ClassName).canonicalName).isEqualTo("$typesPackageName.Person") + assertThat(fn.parameters).hasSize(2) + val arg0 = fn.parameters[0] + assertThat(arg0.name).isEqualTo("name") + assertThat((arg0.type as ClassName).canonicalName).isEqualTo(String::class.qualifiedName) + assertThat(arg0.annotations).hasSize(1) + val arg0Annotation = arg0.annotations[0] + assertThat(arg0Annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.InputArgument") + assertThat(arg0Annotation.members.single().toString()).isEqualTo("\"name\"") + val arg1 = fn.parameters[1] + assertThat(arg1.name).isEqualTo("dataFetchingEnvironment") + assertThat((arg1.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) + } + @Test fun generateDataClassWithNullablePrimitive() { val schema = """ diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt index 8ec1162b8..1f2c99bef 100644 --- a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt @@ -83,6 +83,9 @@ open class GenerateJavaTask @Inject constructor( @Input var generateKotlinClosureProjections = false + @Input + var generateDataFetcherInterfaces = false + @Input var generateDataTypes = true @@ -194,6 +197,7 @@ open class GenerateJavaTask @Inject constructor( generateClientApiv2 = generateClientv2, generateKotlinNullableClasses = generateKotlinNullableClasses, generateKotlinClosureProjections = generateKotlinClosureProjections, + generateDataFetcherInterfaces = generateDataFetcherInterfaces, generateInterfaces = generateInterfaces, generateInterfaceSetters = generateInterfaceSetters, generateInterfaceMethodsForInterfaceFields = generateInterfaceMethodsForInterfaceFields, From 2b7dfa7fc2ce22cc0bee3bbe53d5f331d2287713 Mon Sep 17 00:00:00 2001 From: zaenk Date: Sat, 15 Feb 2025 00:08:28 +0100 Subject: [PATCH 2/5] Generates data fetcher interface for mutation types --- .../netflix/graphql/dgs/codegen/CodeGen.kt | 2 +- .../kotlin/KotlinDataFetcherGenerator.kt | 28 +++++++--- .../graphql/dgs/codegen/KotlinCodeGenTest.kt | 53 +++++++++++++++++-- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt index 049dbbf43..a976c1401 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt @@ -396,7 +396,7 @@ class CodeGen(private val config: CodeGenConfig) { val dataFetchers = if (config.generateDataFetcherInterfaces) { definitions.asSequence() .filterIsInstance() - .filter { it.name == "Query" } + .filter { it.name == "Query" || it.name == "Mutation" } .map { KotlinDataFetcherGenerator(config, document).generate(it) } .fold(CodeGenResult()) { result, next -> result.merge(next) } } else { diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt index 3691cedc6..2d5170750 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt @@ -23,23 +23,35 @@ class KotlinDataFetcherGenerator(private val config: CodeGenConfig, private val private val typeUtils = KotlinTypeUtils(config.packageNameTypes, config, document) private val dsgConstantsPackageName = config.packageName - fun generate(query: ObjectTypeDefinition): CodeGenResult = - query.fieldDefinitions - .map { generateField(it) } + fun generate(topLevelObject: ObjectTypeDefinition): CodeGenResult = + topLevelObject.fieldDefinitions + .map { generateField(it, topLevelObject.name) } .fold(CodeGenResult()) { left, right -> left.merge(right) } - private fun generateField(field: FieldDefinition): CodeGenResult { + private fun generateField(field: FieldDefinition, topLevelObjectName: String): CodeGenResult { val fieldName = field.name.capitalized() - val className = fieldName + "Datafetcher" + val className = fieldName + topLevelObjectName val returnType = typeUtils.findReturnType(field.type) + val annotationParentType = when (topLevelObjectName) { + "Query" -> "DgsConstants.QUERY.TYPE_NAME" + "Mutation" -> "DgsConstants.MUTATION.TYPE_NAME" + else -> error("not supported top level object type: $topLevelObjectName") + } + + val annotationFieldName = when (topLevelObjectName) { + "Query" -> "DgsConstants.QUERY.$fieldName" + "Mutation" -> "DgsConstants.MUTATION.$fieldName" + else -> error("not supported top level object type: $topLevelObjectName") + } + val dsgDataAnnotation = AnnotationSpec.builder(DgsData::class) - .addMember("parentType = DgsConstants.QUERY.TYPE_NAME") - .addMember("field = DgsConstants.QUERY.$fieldName") + .addMember("parentType = $annotationParentType") + .addMember("field = $annotationFieldName") .build() - val methodSpec = FunSpec.builder("get$fieldName") + val methodSpec = FunSpec.builder("${field.name}") .addAnnotation(dsgDataAnnotation) .addModifiers(KModifier.ABSTRACT) .addInputArguments(field) diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt index 4b5aadf66..3d71cf4eb 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt @@ -96,7 +96,7 @@ class KotlinCodeGenTest { ).generate().kotlinDataFetchers assertThat(dataFetchers.size).isEqualTo(1) - assertThat(dataFetchers[0].name).isEqualTo("PeopleDatafetcher") + assertThat(dataFetchers[0].name).isEqualTo("PeopleQuery") assertThat(dataFetchers[0].packageName).isEqualTo(datafetchersPackageName) val type = dataFetchers[0].members[0] as TypeSpec @@ -106,7 +106,7 @@ class KotlinCodeGenTest { }) assertThat(type.funSpecs).hasSize(1) val fn = type.funSpecs.single() - assertThat(fn.name).isEqualTo("getPeople") + assertThat(fn.name).isEqualTo("people") val returnType = fn.returnType as ParameterizedTypeName assertThat(fn.returnType) assertThat(returnType.rawType.canonicalName).isEqualTo(List::class.qualifiedName) @@ -149,14 +149,14 @@ class KotlinCodeGenTest { ).generate().kotlinDataFetchers assertThat(dataFetchers.size).isEqualTo(1) - assertThat(dataFetchers[0].name).isEqualTo("PersonDatafetcher") + assertThat(dataFetchers[0].name).isEqualTo("PersonQuery") assertThat(dataFetchers[0].packageName).isEqualTo(datafetchersPackageName) val type = dataFetchers[0].members[0] as TypeSpec assertThat(type.kind).isEqualTo(TypeSpec.Kind.INTERFACE) assertThat(type.funSpecs).hasSize(1) val fn = type.funSpecs.single() - assertThat(fn.name).isEqualTo("getPerson") + assertThat(fn.name).isEqualTo("person") assertThat((fn.returnType as ClassName).canonicalName).isEqualTo("$typesPackageName.Person") assertThat(fn.parameters).hasSize(2) val arg0 = fn.parameters[0] @@ -171,6 +171,51 @@ class KotlinCodeGenTest { assertThat((arg1.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) } + @Test + fun generateMutationInterfaceWithArgument() { + val schema = """ + type Mutation { + addPerson(person: Person): Person + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + val dataFetchers = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + language = Language.KOTLIN, + generateDataFetcherInterfaces = true + ) + ).generate().kotlinDataFetchers + + assertThat(dataFetchers.size).isEqualTo(1) + assertThat(dataFetchers[0].name).isEqualTo("AddPersonMutation") + assertThat(dataFetchers[0].packageName).isEqualTo(datafetchersPackageName) + val type = dataFetchers[0].members[0] as TypeSpec + + assertThat(type.kind).isEqualTo(TypeSpec.Kind.INTERFACE) + assertThat(type.funSpecs).hasSize(1) + val fn = type.funSpecs.single() + assertThat(fn.name).isEqualTo("addPerson") + assertThat((fn.returnType as ClassName).canonicalName).isEqualTo("$typesPackageName.Person") + assertThat(fn.parameters).hasSize(2) + val arg0 = fn.parameters[0] + assertThat(arg0.name).isEqualTo("person") + assertThat((arg0.type as ClassName).canonicalName).isEqualTo("$typesPackageName.Person") + assertThat(arg0.annotations).hasSize(1) + val arg0Annotation = arg0.annotations[0] + assertThat(arg0Annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.InputArgument") + assertThat(arg0Annotation.members.single().toString()).isEqualTo("\"person\"") + val arg1 = fn.parameters[1] + assertThat(arg1.name).isEqualTo("dataFetchingEnvironment") + assertThat((arg1.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) + } + @Test fun generateDataClassWithNullablePrimitive() { val schema = """ From ff65d834b1929d3b427b1539747403e052d82015 Mon Sep 17 00:00:00 2001 From: zaenk Date: Sat, 15 Feb 2025 00:28:30 +0100 Subject: [PATCH 3/5] Generates data fetcher interface for subscription types --- .../netflix/graphql/dgs/codegen/CodeGen.kt | 2 +- .../kotlin/KotlinDataFetcherGenerator.kt | 11 ++++- .../graphql/dgs/codegen/KotlinCodeGenTest.kt | 46 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt index a976c1401..e36053ff5 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt @@ -396,7 +396,7 @@ class CodeGen(private val config: CodeGenConfig) { val dataFetchers = if (config.generateDataFetcherInterfaces) { definitions.asSequence() .filterIsInstance() - .filter { it.name == "Query" || it.name == "Mutation" } + .filter { it.name == "Query" || it.name == "Mutation" || it.name == "Subscription" } .map { KotlinDataFetcherGenerator(config, document).generate(it) } .fold(CodeGenResult()) { result, next -> result.merge(next) } } else { diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt index 2d5170750..17e019e6b 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt @@ -7,10 +7,12 @@ import com.netflix.graphql.dgs.codegen.CodeGenConfig import com.netflix.graphql.dgs.codegen.CodeGenResult import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.TypeSpec import graphql.language.Document import graphql.language.FieldDefinition @@ -32,17 +34,24 @@ class KotlinDataFetcherGenerator(private val config: CodeGenConfig, private val val fieldName = field.name.capitalized() val className = fieldName + topLevelObjectName - val returnType = typeUtils.findReturnType(field.type) + val returnType = if (topLevelObjectName == "Subscription") { + val genericType = typeUtils.findReturnType(field.type) + ClassName.bestGuess("org.reactivestreams.Publisher").parameterizedBy(genericType) + } else { + typeUtils.findReturnType(field.type) + } val annotationParentType = when (topLevelObjectName) { "Query" -> "DgsConstants.QUERY.TYPE_NAME" "Mutation" -> "DgsConstants.MUTATION.TYPE_NAME" + "Subscription" -> "DgsConstants.SUBSCRIPTION.TYPE_NAME" else -> error("not supported top level object type: $topLevelObjectName") } val annotationFieldName = when (topLevelObjectName) { "Query" -> "DgsConstants.QUERY.$fieldName" "Mutation" -> "DgsConstants.MUTATION.$fieldName" + "Subscription" -> "DgsConstants.SUBSCRIPTION.$fieldName" else -> error("not supported top level object type: $topLevelObjectName") } diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt index 3d71cf4eb..02baae868 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt @@ -216,6 +216,52 @@ class KotlinCodeGenTest { assertThat((arg1.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) } + @Test + fun generateSubscriptionInterfaceWithArgument() { + val schema = """ + type Subscription { + personUpdated(id: Int): Person + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + val dataFetchers = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + language = Language.KOTLIN, + generateDataFetcherInterfaces = true + ) + ).generate().kotlinDataFetchers + + assertThat(dataFetchers.size).isEqualTo(1) + assertThat(dataFetchers[0].name).isEqualTo("PersonUpdatedSubscription") + assertThat(dataFetchers[0].packageName).isEqualTo(datafetchersPackageName) + val type = dataFetchers[0].members[0] as TypeSpec + + assertThat(type.kind).isEqualTo(TypeSpec.Kind.INTERFACE) + assertThat(type.funSpecs).hasSize(1) + val fn = type.funSpecs.single() + assertThat(fn.name).isEqualTo("personUpdated") + assertThat((fn.returnType as ParameterizedTypeName).rawType.canonicalName).isEqualTo("org.reactivestreams.Publisher") + assertThat((fn.returnType as ParameterizedTypeName).typeArguments[0].toString()).isEqualTo("$typesPackageName.Person?") + assertThat(fn.parameters).hasSize(2) + val arg0 = fn.parameters[0] + assertThat(arg0.name).isEqualTo("id") + assertThat((arg0.type as ClassName).canonicalName).isEqualTo("kotlin.Int") + assertThat(arg0.annotations).hasSize(1) + val arg0Annotation = arg0.annotations[0] + assertThat(arg0Annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.InputArgument") + assertThat(arg0Annotation.members.single().toString()).isEqualTo("\"id\"") + val arg1 = fn.parameters[1] + assertThat(arg1.name).isEqualTo("dataFetchingEnvironment") + assertThat((arg1.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) + } + @Test fun generateDataClassWithNullablePrimitive() { val schema = """ From 70d97bfd4cbd4d47364d39ae676a48d3e4160c15 Mon Sep 17 00:00:00 2001 From: zaenk Date: Sat, 15 Feb 2025 00:40:15 +0100 Subject: [PATCH 4/5] Replace strings with DgsConstants in annotations of generated interfaces --- .../kotlin/KotlinDataFetcherGenerator.kt | 23 ++++++++----------- .../graphql/dgs/codegen/KotlinCodeGenTest.kt | 6 ++--- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt index 17e019e6b..aeadba7d8 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherGenerator.kt @@ -41,19 +41,16 @@ class KotlinDataFetcherGenerator(private val config: CodeGenConfig, private val typeUtils.findReturnType(field.type) } - val annotationParentType = when (topLevelObjectName) { - "Query" -> "DgsConstants.QUERY.TYPE_NAME" - "Mutation" -> "DgsConstants.MUTATION.TYPE_NAME" - "Subscription" -> "DgsConstants.SUBSCRIPTION.TYPE_NAME" + val dsgConstantPrefix = when (topLevelObjectName) { + "Query" -> "DgsConstants.QUERY" + "Mutation" -> "DgsConstants.MUTATION" + "Subscription" -> "DgsConstants.SUBSCRIPTION" else -> error("not supported top level object type: $topLevelObjectName") } - val annotationFieldName = when (topLevelObjectName) { - "Query" -> "DgsConstants.QUERY.$fieldName" - "Mutation" -> "DgsConstants.MUTATION.$fieldName" - "Subscription" -> "DgsConstants.SUBSCRIPTION.$fieldName" - else -> error("not supported top level object type: $topLevelObjectName") - } + val annotationParentType = "$dsgConstantPrefix.TYPE_NAME" + + val annotationFieldName = "$dsgConstantPrefix.$fieldName" val dsgDataAnnotation = AnnotationSpec.builder(DgsData::class) .addMember("parentType = $annotationParentType") @@ -63,7 +60,7 @@ class KotlinDataFetcherGenerator(private val config: CodeGenConfig, private val val methodSpec = FunSpec.builder("${field.name}") .addAnnotation(dsgDataAnnotation) .addModifiers(KModifier.ABSTRACT) - .addInputArguments(field) + .addInputArguments(field, dsgConstantPrefix) .addParameter("dataFetchingEnvironment", DataFetchingEnvironment::class) .returns(returnType) .build() @@ -81,10 +78,10 @@ class KotlinDataFetcherGenerator(private val config: CodeGenConfig, private val return CodeGenResult(kotlinDataFetchers = listOf(fileSpec)) } - private fun FunSpec.Builder.addInputArguments(field: FieldDefinition): FunSpec.Builder = apply { + private fun FunSpec.Builder.addInputArguments(field: FieldDefinition, prefix: String): FunSpec.Builder = apply { field.inputValueDefinitions.forEach { input -> val inputAnnotation = AnnotationSpec.builder(InputArgument::class) - .addMember("\"${input.name}\"") + .addMember("$prefix.${field.name.uppercase()}_INPUT_ARGUMENT.${input.name.capitalized()}") .build() val inputType = ParameterSpec.builder(input.name, typeUtils.findReturnType(input.type)) .addAnnotation(inputAnnotation) diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt index 02baae868..dbba803cb 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt @@ -165,7 +165,7 @@ class KotlinCodeGenTest { assertThat(arg0.annotations).hasSize(1) val arg0Annotation = arg0.annotations[0] assertThat(arg0Annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.InputArgument") - assertThat(arg0Annotation.members.single().toString()).isEqualTo("\"name\"") + assertThat(arg0Annotation.members.single().toString()).isEqualTo("DgsConstants.QUERY.PERSON_INPUT_ARGUMENT.Name") val arg1 = fn.parameters[1] assertThat(arg1.name).isEqualTo("dataFetchingEnvironment") assertThat((arg1.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) @@ -210,7 +210,7 @@ class KotlinCodeGenTest { assertThat(arg0.annotations).hasSize(1) val arg0Annotation = arg0.annotations[0] assertThat(arg0Annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.InputArgument") - assertThat(arg0Annotation.members.single().toString()).isEqualTo("\"person\"") + assertThat(arg0Annotation.members.single().toString()).isEqualTo("DgsConstants.MUTATION.ADDPERSON_INPUT_ARGUMENT.Person") val arg1 = fn.parameters[1] assertThat(arg1.name).isEqualTo("dataFetchingEnvironment") assertThat((arg1.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) @@ -256,7 +256,7 @@ class KotlinCodeGenTest { assertThat(arg0.annotations).hasSize(1) val arg0Annotation = arg0.annotations[0] assertThat(arg0Annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.InputArgument") - assertThat(arg0Annotation.members.single().toString()).isEqualTo("\"id\"") + assertThat(arg0Annotation.members.single().toString()).isEqualTo("DgsConstants.SUBSCRIPTION.PERSONUPDATED_INPUT_ARGUMENT.Id") val arg1 = fn.parameters[1] assertThat(arg1.name).isEqualTo("dataFetchingEnvironment") assertThat((arg1.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) From 152685dadc5ea831a4923e26144f577edcb588ae Mon Sep 17 00:00:00 2001 From: zaenk Date: Sat, 15 Feb 2025 21:23:59 +0100 Subject: [PATCH 5/5] Generate data fetcher interfaces when generateKotlinNullableClasses and generateDataFetcherInterfaces is true --- .../netflix/graphql/dgs/codegen/CodeGen.kt | 3 +- .../GenerateKotlin2DataFetcherInterfaces.kt | 38 ++++ .../dgs/codegen/Kotline2CodeGenTest.kt | 196 ++++++++++++++++++ 3 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataFetcherInterfaces.kt diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt index e36053ff5..c56ec0b90 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt @@ -365,7 +365,8 @@ class CodeGen(private val config: CodeGenConfig) { kotlinInputTypes = generateKotlin2InputTypes(config, document, requiredTypes), kotlinInterfaces = generateKotlin2Interfaces(config, document), kotlinEnumTypes = generateKotlin2EnumTypes(config, document, requiredTypes), - kotlinConstants = KotlinConstantsGenerator(config, document).generate().kotlinConstants + kotlinConstants = KotlinConstantsGenerator(config, document).generate().kotlinConstants, + kotlinDataFetchers = generateKotlin2DataFetcherInterfaces(config, document) ) } else { val datatypesResult = generateKotlinDataTypes(definitions) diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataFetcherInterfaces.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataFetcherInterfaces.kt new file mode 100644 index 000000000..169ea6ae0 --- /dev/null +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataFetcherInterfaces.kt @@ -0,0 +1,38 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.graphql.dgs.codegen.generators.kotlin2 + +import com.netflix.graphql.dgs.codegen.CodeGenConfig +import com.netflix.graphql.dgs.codegen.CodeGenResult +import com.netflix.graphql.dgs.codegen.generators.kotlin.KotlinDataFetcherGenerator +import com.squareup.kotlinpoet.FileSpec +import graphql.language.Document +import graphql.language.ObjectTypeDefinition + +fun generateKotlin2DataFetcherInterfaces(config: CodeGenConfig, document: Document): List = + if (config.generateDataFetcherInterfaces) { + document.definitions.asSequence() + .filterIsInstance() + .filter { it.name == "Query" || it.name == "Mutation" || it.name == "Subscription" } + .map { KotlinDataFetcherGenerator(config, document).generate(it) } + .fold(CodeGenResult()) { result, next -> result.merge(next) } + .kotlinDataFetchers + } else { + emptyList() + } diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/Kotline2CodeGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/Kotline2CodeGenTest.kt index c17ab3874..a1f743112 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/Kotline2CodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/Kotline2CodeGenTest.kt @@ -18,9 +18,12 @@ package com.netflix.graphql.dgs.codegen +import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.asClassName +import graphql.schema.DataFetchingEnvironment import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import java.io.Serializable @@ -134,4 +137,197 @@ class Kotline2CodeGenTest { assertCompilesKotlin(result.kotlinEnumTypes) } + + @Test + fun generateDataFetcherInterfaceWithFunction() { + val schema = """ + type Query { + people: [Person] + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + val dataFetchers = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + generateDataFetcherInterfaces = true + ) + ).generate().kotlinDataFetchers + + assertThat(dataFetchers.size).isEqualTo(1) + assertThat(dataFetchers[0].name).isEqualTo("PeopleQuery") + assertThat(dataFetchers[0].packageName).isEqualTo("$basePackageName.datafetchers") + val type = dataFetchers[0].members[0] as TypeSpec + + assertThat(type.kind).isEqualTo(TypeSpec.Kind.INTERFACE) + assertThat(type.annotations).hasSize(1).first().satisfies({ + assertThat(it.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.DgsComponent") + }) + assertThat(type.funSpecs).hasSize(1) + val fn = type.funSpecs.single() + assertThat(fn.name).isEqualTo("people") + val returnType = fn.returnType as ParameterizedTypeName + assertThat(fn.returnType) + assertThat(returnType.rawType.canonicalName).isEqualTo(List::class.qualifiedName) + assertThat(returnType.typeArguments).hasSize(1) + val arg0 = returnType.typeArguments.single() as ClassName + assertThat(arg0.canonicalName).isEqualTo("$typesPackageName.Person") + assertThat(fn.parameters).hasSize(1) + val param0 = fn.parameters.single() + assertThat(param0.name).isEqualTo("dataFetchingEnvironment") + assertThat((param0.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) + assertThat(fn.annotations).hasSize(1).first().satisfies({ annotation -> + assertThat(annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.DgsData") + assertThat(annotation.members).satisfiesExactly( + { member -> assertThat(member.toString()).isEqualTo("parentType = DgsConstants.QUERY.TYPE_NAME") }, + { member -> assertThat(member.toString()).isEqualTo("field = DgsConstants.QUERY.People") } + ) + }) + } + + @Test + fun generateDataFetcherInterfaceWithArgument() { + val schema = """ + type Query { + person(name: String): Person + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + val dataFetchers = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + generateDataFetcherInterfaces = true + ) + ).generate().kotlinDataFetchers + + assertThat(dataFetchers.size).isEqualTo(1) + assertThat(dataFetchers[0].name).isEqualTo("PersonQuery") + assertThat(dataFetchers[0].packageName).isEqualTo("$basePackageName.datafetchers") + val type = dataFetchers[0].members[0] as TypeSpec + + assertThat(type.kind).isEqualTo(TypeSpec.Kind.INTERFACE) + assertThat(type.funSpecs).hasSize(1) + val fn = type.funSpecs.single() + assertThat(fn.name).isEqualTo("person") + assertThat((fn.returnType as ClassName).canonicalName).isEqualTo("$typesPackageName.Person") + assertThat(fn.parameters).hasSize(2) + val arg0 = fn.parameters[0] + assertThat(arg0.name).isEqualTo("name") + assertThat((arg0.type as ClassName).canonicalName).isEqualTo(String::class.qualifiedName) + assertThat(arg0.annotations).hasSize(1) + val arg0Annotation = arg0.annotations[0] + assertThat(arg0Annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.InputArgument") + assertThat(arg0Annotation.members.single().toString()).isEqualTo("DgsConstants.QUERY.PERSON_INPUT_ARGUMENT.Name") + val arg1 = fn.parameters[1] + assertThat(arg1.name).isEqualTo("dataFetchingEnvironment") + assertThat((arg1.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) + } + + @Test + fun generateMutationInterfaceWithArgument() { + val schema = """ + type Mutation { + addPerson(person: Person): Person + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + val dataFetchers = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + generateDataFetcherInterfaces = true + ) + ).generate().kotlinDataFetchers + + assertThat(dataFetchers.size).isEqualTo(1) + assertThat(dataFetchers[0].name).isEqualTo("AddPersonMutation") + assertThat(dataFetchers[0].packageName).isEqualTo("$basePackageName.datafetchers") + val type = dataFetchers[0].members[0] as TypeSpec + + assertThat(type.kind).isEqualTo(TypeSpec.Kind.INTERFACE) + assertThat(type.funSpecs).hasSize(1) + val fn = type.funSpecs.single() + assertThat(fn.name).isEqualTo("addPerson") + assertThat((fn.returnType as ClassName).canonicalName).isEqualTo("$typesPackageName.Person") + assertThat(fn.parameters).hasSize(2) + val arg0 = fn.parameters[0] + assertThat(arg0.name).isEqualTo("person") + assertThat((arg0.type as ClassName).canonicalName).isEqualTo("$typesPackageName.Person") + assertThat(arg0.annotations).hasSize(1) + val arg0Annotation = arg0.annotations[0] + assertThat(arg0Annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.InputArgument") + assertThat(arg0Annotation.members.single().toString()).isEqualTo("DgsConstants.MUTATION.ADDPERSON_INPUT_ARGUMENT.Person") + val arg1 = fn.parameters[1] + assertThat(arg1.name).isEqualTo("dataFetchingEnvironment") + assertThat((arg1.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) + } + + @Test + fun generateSubscriptionInterfaceWithArgument() { + val schema = """ + type Subscription { + personUpdated(id: Int): Person + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + val dataFetchers = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + generateDataFetcherInterfaces = true + ) + ).generate().kotlinDataFetchers + + assertThat(dataFetchers.size).isEqualTo(1) + assertThat(dataFetchers[0].name).isEqualTo("PersonUpdatedSubscription") + assertThat(dataFetchers[0].packageName).isEqualTo("$basePackageName.datafetchers") + val type = dataFetchers[0].members[0] as TypeSpec + + assertThat(type.kind).isEqualTo(TypeSpec.Kind.INTERFACE) + assertThat(type.funSpecs).hasSize(1) + val fn = type.funSpecs.single() + assertThat(fn.name).isEqualTo("personUpdated") + assertThat((fn.returnType as ParameterizedTypeName).rawType.canonicalName).isEqualTo("org.reactivestreams.Publisher") + assertThat((fn.returnType as ParameterizedTypeName).typeArguments[0].toString()).isEqualTo("$typesPackageName.Person?") + assertThat(fn.parameters).hasSize(2) + val arg0 = fn.parameters[0] + assertThat(arg0.name).isEqualTo("id") + assertThat((arg0.type as ClassName).canonicalName).isEqualTo("kotlin.Int") + assertThat(arg0.annotations).hasSize(1) + val arg0Annotation = arg0.annotations[0] + assertThat(arg0Annotation.typeName.toString()).isEqualTo("com.netflix.graphql.dgs.InputArgument") + assertThat(arg0Annotation.members.single().toString()).isEqualTo("DgsConstants.SUBSCRIPTION.PERSONUPDATED_INPUT_ARGUMENT.Id") + val arg1 = fn.parameters[1] + assertThat(arg1.name).isEqualTo("dataFetchingEnvironment") + assertThat((arg1.type as ClassName).canonicalName).isEqualTo(DataFetchingEnvironment::class.qualifiedName) + } }