From 76a529d6ca344f5943c64fc8d131f0b8c858903d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Carl=C3=A9?= Date: Fri, 11 Feb 2022 19:28:26 +0100 Subject: [PATCH 1/3] feature: read and use operation types from schema --- .../netflix/graphql/dgs/codegen/CodeGen.kt | 14 ++--- .../graphql/dgs/codegen/OperationTypes.kt | 53 +++++++++++++++++++ .../dgs/codegen/RequiredTypeCollector.kt | 11 ++-- .../generators/java/ConstantsGenerator.kt | 7 +-- .../generators/java/DatafetcherGenerator.kt | 3 +- .../kotlin/KotlinConstantsGenerator.kt | 7 +-- .../graphql/dgs/codegen/CodeGenTest.kt | 32 +++++++++++ .../netflix/graphql/dgs/codegen/TestUtils.kt | 1 + 8 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/OperationTypes.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 a47aa60f0..148bbc949 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 @@ -161,7 +161,7 @@ class CodeGen(private val config: CodeGenConfig) { return if (config.generateClientApi) { definitions.asSequence() .filterIsInstance() - .filter { it.name == "Query" || it.name == "Mutation" || it.name == "Subscription" } + .filter { OperationTypes.isOperationType(it.name) } .map { ClientApiGenerator(config, document).generate(it) } .fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) } } else CodeGenResult() @@ -192,7 +192,7 @@ class CodeGen(private val config: CodeGenConfig) { private fun generateJavaDataFetchers(definitions: Collection>): CodeGenResult { return definitions.asSequence() .filterIsInstance() - .filter { it.name == "Query" } + .filter { it.name == OperationTypes.query } .map { DatafetcherGenerator(config, document).generate(it) } .fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) } } @@ -201,7 +201,7 @@ class CodeGen(private val config: CodeGenConfig) { return definitions.asSequence() .filterIsInstance() .excludeSchemaTypeExtension() - .filter { it.name != "Query" && it.name != "Mutation" && it.name != "RelayPageInfo" } + .filter { it.name != OperationTypes.query && it.name != OperationTypes.mutation && it.name != "RelayPageInfo" } .filter { config.generateInterfaces || config.generateDataTypes || it.name in requiredTypeCollector.requiredTypes } .map { DataTypeGenerator(config, document).generate(it, findTypeExtensions(it.name, definitions)) @@ -299,7 +299,7 @@ class CodeGen(private val config: CodeGenConfig) { return definitions.asSequence() .filterIsInstance() .excludeSchemaTypeExtension() - .filter { it.name != "Query" && it.name != "Mutation" && it.name != "RelayPageInfo" } + .filter { it.name != OperationTypes.query && it.name != OperationTypes.mutation && it.name != "RelayPageInfo" } .filter { config.generateDataTypes || it.name in requiredTypeCollector.requiredTypes } .map { val extensions = findTypeExtensions(it.name, definitions) @@ -445,21 +445,21 @@ fun List.filterSkipped(): List { fun List.filterIncludedInConfig(definitionName: String, config: CodeGenConfig): List { return when (definitionName) { - "Query" -> { + OperationTypes.query -> { if (config.includeQueries.isEmpty()) { this } else { this.filter { it.name in config.includeQueries } } } - "Mutation" -> { + OperationTypes.mutation -> { if (config.includeMutations.isEmpty()) { this } else { this.filter { it.name in config.includeMutations } } } - "Subscription" -> { + OperationTypes.subscription -> { if (config.includeSubscriptions.isEmpty()) { this } else { diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/OperationTypes.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/OperationTypes.kt new file mode 100644 index 000000000..f501a8806 --- /dev/null +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/OperationTypes.kt @@ -0,0 +1,53 @@ +/* + * + * 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 + +import graphql.language.Document +import graphql.language.SchemaDefinition + +object OperationTypes { + fun initialize(document: Document) { + restoreDefaults() + val schemaDefinitionList = document.definitions.filterIsInstance() + + if (schemaDefinitionList.isNotEmpty()) { + val schemaDefinition = schemaDefinitionList.last() + schemaDefinition.operationTypeDefinitions + .forEach { + when (it.name) { + "query" -> query = it.typeName.name + "mutation" -> mutation = it.typeName.name + "subscription" -> subscription = it.typeName.name + } + } + } + } + + private fun restoreDefaults() { + query = "Query" + mutation = "Mutation" + subscription = "Subscription" + } + + fun isOperationType(typeName: String) = typeName == query || typeName == mutation || typeName == subscription + + var query = "Query" + var mutation = "Mutation" + var subscription = "Subscription" +} \ No newline at end of file diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/RequiredTypeCollector.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/RequiredTypeCollector.kt index 34a263e2e..4211f978e 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/RequiredTypeCollector.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/RequiredTypeCollector.kt @@ -42,11 +42,14 @@ class RequiredTypeCollector( init { val fieldDefinitions = mutableListOf() + + OperationTypes.initialize(document) + for (definition in document.definitions.asSequence().filterIsInstance()) { when (definition.name) { - "Query" -> definition.fieldDefinitions.filterTo(fieldDefinitions) { it.name in queries } - "Mutation" -> definition.fieldDefinitions.filterTo(fieldDefinitions) { it.name in mutations } - "Subscription" -> definition.fieldDefinitions.filterTo(fieldDefinitions) { it.name in subscriptions } + OperationTypes.query -> definition.fieldDefinitions.filterTo(fieldDefinitions) { it.name in queries } + OperationTypes.mutation -> definition.fieldDefinitions.filterTo(fieldDefinitions) { it.name in mutations } + OperationTypes.subscription -> definition.fieldDefinitions.filterTo(fieldDefinitions) { it.name in subscriptions } } } @@ -127,4 +130,4 @@ class RequiredTypeCollector( fieldDefinitions ) } -} +} \ No newline at end of file diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ConstantsGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ConstantsGenerator.kt index 18c5444d6..fc8981696 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ConstantsGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ConstantsGenerator.kt @@ -20,6 +20,7 @@ package com.netflix.graphql.dgs.codegen.generators.java import com.netflix.graphql.dgs.codegen.CodeGenConfig import com.netflix.graphql.dgs.codegen.CodeGenResult +import com.netflix.graphql.dgs.codegen.OperationTypes import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized import com.netflix.graphql.dgs.codegen.generators.shared.SchemaExtensionsUtils.findInputExtensions @@ -103,13 +104,13 @@ class ConstantsGenerator(private val config: CodeGenConfig, private val document constantsType.addField(FieldSpec.builder(TypeName.get(String::class.java), "TYPE_NAME").addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL).initializer(""""${it.name}"""").build()) } - if (document.definitions.any { it is ObjectTypeDefinition && it.name == "Query" }) { + if (document.definitions.any { it is ObjectTypeDefinition && it.name == OperationTypes.query }) { javaType.addField(FieldSpec.builder(TypeName.get(String::class.java), "QUERY_TYPE").addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL).initializer(""""Query"""").build()) } - if (document.definitions.any { it is ObjectTypeDefinition && it.name == "MUTATION" }) { + if (document.definitions.any { it is ObjectTypeDefinition && it.name == OperationTypes.mutation }) { javaType.addField(FieldSpec.builder(TypeName.get(String::class.java), "MUTATION_TYPE").addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL).initializer(""""Mutation"""").build()) } - if (document.definitions.any { it is ObjectTypeDefinition && it.name == "Subscription" }) { + if (document.definitions.any { it is ObjectTypeDefinition && it.name == OperationTypes.subscription }) { javaType.addField(FieldSpec.builder(TypeName.get(String::class.java), "SUBSCRIPTION_TYPE").addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL).initializer(""""Subscription"""").build()) } diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DatafetcherGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DatafetcherGenerator.kt index 57fe38cf6..f3892b319 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DatafetcherGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DatafetcherGenerator.kt @@ -22,6 +22,7 @@ import com.netflix.graphql.dgs.DgsComponent import com.netflix.graphql.dgs.DgsData import com.netflix.graphql.dgs.codegen.CodeGenConfig import com.netflix.graphql.dgs.codegen.CodeGenResult +import com.netflix.graphql.dgs.codegen.OperationTypes import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized import com.squareup.javapoet.AnnotationSpec import com.squareup.javapoet.JavaFile @@ -60,7 +61,7 @@ class DatafetcherGenerator(private val config: CodeGenConfig, private val docume val methodSpec = MethodSpec.methodBuilder("get$fieldName") .returns(returnType) .addModifiers(Modifier.PUBLIC) - .addAnnotation(AnnotationSpec.builder(DgsData::class.java).addMember("parentType", "\$S", "Query").addMember("field", "\$S", field.name).build()) + .addAnnotation(AnnotationSpec.builder(DgsData::class.java).addMember("parentType", "\$S", OperationTypes.query).addMember("field", "\$S", field.name).build()) .addParameter(ParameterSpec.builder(DataFetchingEnvironment::class.java, "dataFetchingEnvironment").build()) .addStatement("return $returnValue") diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinConstantsGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinConstantsGenerator.kt index 929117a0d..85ea72658 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinConstantsGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinConstantsGenerator.kt @@ -20,6 +20,7 @@ package com.netflix.graphql.dgs.codegen.generators.kotlin import com.netflix.graphql.dgs.codegen.CodeGenConfig import com.netflix.graphql.dgs.codegen.CodeGenResult +import com.netflix.graphql.dgs.codegen.OperationTypes import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized import com.netflix.graphql.dgs.codegen.generators.shared.SchemaExtensionsUtils @@ -99,15 +100,15 @@ class KotlinConstantsGenerator(private val config: CodeGenConfig, private val do baseConstantsType.addType(constantsType.build()) } - if (document.definitions.any { it is ObjectTypeDefinition && it.name == "Query" }) { + if (document.definitions.any { it is ObjectTypeDefinition && it.name == OperationTypes.query }) { baseConstantsType.addProperty(PropertySpec.builder("QUERY_TYPE", String::class).addModifiers(KModifier.CONST).initializer(""""Query"""").build()) } - if (document.definitions.any { it is ObjectTypeDefinition && it.name == "Mutation" }) { + if (document.definitions.any { it is ObjectTypeDefinition && it.name == OperationTypes.mutation }) { baseConstantsType.addProperty(PropertySpec.builder("Mutation_TYPE", String::class).addModifiers(KModifier.CONST).initializer(""""Mutation"""").build()) } - if (document.definitions.any { it is ObjectTypeDefinition && it.name == "Subscription" }) { + if (document.definitions.any { it is ObjectTypeDefinition && it.name == OperationTypes.subscription }) { baseConstantsType.addProperty(PropertySpec.builder("Subscription_TYPE", String::class).addModifiers(KModifier.CONST).initializer(""""Subscription"""").build()) } diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt index b3f7ed2b6..4f038a470 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt @@ -2620,6 +2620,38 @@ It takes a title and such. ) } + @Test + fun generateUsingSchemaDefinitions() { + val schema = """ + schema { + query: SomeQuery + mutation: SomeMutation + } + type SomeQuery { + people: [Person] + } + type SomeMutation { + addPerson: Boolean + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + val (dataTypes) = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + ) + ).generate() + + assertThat(dataTypes.size).isEqualTo(1) + val typeSpec = dataTypes[0].typeSpec + assertThat(typeSpec.name).isEqualTo("Person") + } + @Test fun generateInterfaceJavaDoc() { val schema = """ diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/TestUtils.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/TestUtils.kt index 9d6fe8490..6d9bbc840 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/TestUtils.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/TestUtils.kt @@ -47,6 +47,7 @@ fun assertCompilesJava(codeGenResult: CodeGenResult): Compilation { } fun assertCompilesJava(javaFiles: Collection): Compilation { + println(javaFiles.map { it.toString() }) val result = javac() .withOptions("-parameters") .compile(javaFiles.map(JavaFile::toJavaFileObject)) From 679a29879e267bc8429d2f6959afd7f4b8010d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Carl=C3=A9?= Date: Sun, 13 Feb 2022 01:04:46 +0100 Subject: [PATCH 2/3] feature(java): add config and if active generate data fetcher interfaces instead of basic example data fetchers --- .../netflix/graphql/dgs/codegen/CodeGen.kt | 18 +- .../netflix/graphql/dgs/codegen/CodeGenCli.kt | 7 +- .../java/DataFetcherInterfaceGenerator.kt | 168 +++++++++++++ .../java/DataFetcherInterfaceGeneratorTest.kt | 238 ++++++++++++++++++ .../dgs/codegen/gradle/GenerateJavaTask.kt | 4 + 5 files changed, 428 insertions(+), 7 deletions(-) create mode 100644 graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataFetcherInterfaceGenerator.kt create mode 100644 graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataFetcherInterfaceGeneratorTest.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 148bbc949..3d949a139 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 @@ -190,11 +190,18 @@ class CodeGen(private val config: CodeGenConfig) { } private fun generateJavaDataFetchers(definitions: Collection>): CodeGenResult { - return definitions.asSequence() - .filterIsInstance() - .filter { it.name == OperationTypes.query } - .map { DatafetcherGenerator(config, document).generate(it) } - .fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) } + return if (config.generateDataFetchersAsInterfaces) { + definitions.asSequence() + .filterIsInstance() + .map { DataFetcherInterfaceGenerator(config, document).generate(it) } + .fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) } + } else { + definitions.asSequence() + .filterIsInstance() + .filter { it.name == OperationTypes.query } + .map { DatafetcherGenerator(config, document).generate(it) } + .fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) } + } } private fun generateJavaDataType(definitions: Collection>): CodeGenResult { @@ -336,6 +343,7 @@ data class CodeGenConfig( /** If enabled, the names of the classes available via the DgsConstant class will be snake cased.*/ val snakeCaseConstantNames: Boolean = false, val generateInterfaceSetters: Boolean = true, + val generateDataFetchersAsInterfaces: Boolean = false, ) { val packageNameClient: String get() = "$packageName.$subPackageNameClient" diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGenCli.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGenCli.kt index 94cfcf479..1d55d5e3f 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGenCli.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGenCli.kt @@ -58,6 +58,7 @@ class CodeGenCli : CliktCommand("Generate Java sources for SCHEMA file(s)") { private val typeMapping: Map by option("--type-mapping").associate() private val shortProjectionNames by option("--short-projection-names").flag() private val generateInterfaceSetters by option("--generate-interface-setters").flag() + private val generateDataFetchersAsInterfaces by option("--generate-datafetchers-as-interfaces", help = "Generate the data fetchers as interfaces instead of generating basic examples").flag(default = false) override fun run() { val inputSchemas = if (schemas.isEmpty()) { @@ -92,7 +93,8 @@ class CodeGenCli : CliktCommand("Generate Java sources for SCHEMA file(s)") { shortProjectionNames = shortProjectionNames, generateDataTypes = generateDataTypes, generateInterfaces = generateInterfaces, - generateInterfaceSetters = generateInterfaceSetters + generateInterfaceSetters = generateInterfaceSetters, + generateDataFetchersAsInterfaces = generateDataFetchersAsInterfaces ) } else { CodeGenConfig( @@ -112,7 +114,8 @@ class CodeGenCli : CliktCommand("Generate Java sources for SCHEMA file(s)") { shortProjectionNames = shortProjectionNames, generateDataTypes = generateDataTypes, generateInterfaces = generateInterfaces, - generateInterfaceSetters = generateInterfaceSetters + generateInterfaceSetters = generateInterfaceSetters, + generateDataFetchersAsInterfaces = generateDataFetchersAsInterfaces ) } ).generate() diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataFetcherInterfaceGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataFetcherInterfaceGenerator.kt new file mode 100644 index 000000000..d05ddd0ca --- /dev/null +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataFetcherInterfaceGenerator.kt @@ -0,0 +1,168 @@ +/* + * + * 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.java + +import com.netflix.graphql.dgs.* +import com.netflix.graphql.dgs.codegen.CodeGenConfig +import com.netflix.graphql.dgs.codegen.CodeGenResult +import com.netflix.graphql.dgs.codegen.OperationTypes +import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized +import com.netflix.graphql.dgs.codegen.isBaseType +import com.squareup.javapoet.AnnotationSpec +import com.squareup.javapoet.ClassName +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.MethodSpec +import com.squareup.javapoet.ParameterSpec +import com.squareup.javapoet.ParameterizedTypeName +import com.squareup.javapoet.TypeSpec +import graphql.execution.UnknownOperationException +import graphql.language.Document +import graphql.language.FieldDefinition +import graphql.language.InputValueDefinition +import graphql.language.ListType +import graphql.language.NonNullType +import graphql.language.ObjectTypeDefinition +import graphql.language.Type +import graphql.language.TypeName +import org.reactivestreams.Publisher +import java.util.concurrent.CompletableFuture +import javax.lang.model.element.Modifier + +class DataFetcherInterfaceGenerator(private val config: CodeGenConfig, private val document: Document) { + fun generate(objectTypeDefinition: ObjectTypeDefinition): CodeGenResult { + val isOperationType = OperationTypes.isOperationType(objectTypeDefinition.name) + val fields = if (!isOperationType) { + objectTypeDefinition.fieldDefinitions.filter { !it.type.isBaseType() && !it.type.isID() } + } else { + objectTypeDefinition.fieldDefinitions + } + + if (fields.isNullOrEmpty()) { + return CodeGenResult() + } + + return createDataFetcherInterface( + objectTypeDefinition, + if (!isOperationType) { + fields.map { field -> + createDataFetcherInterfaceMethodForType(field, objectTypeDefinition) + } + } else { + fields.map { field -> + createDataFetcherInterfaceMethodForOperation(field, objectTypeDefinition) + } + } + ) + } + + private fun createDataFetcherInterface( + objectTypeDefinition: ObjectTypeDefinition, + methods: List + ): CodeGenResult { + val javaType = TypeSpec.interfaceBuilder(objectTypeDefinition.name.capitalized() + "DataFetcher") + .addModifiers(Modifier.PUBLIC) + .addMethods(methods) + + val javaFile = JavaFile.builder(getPackageName(), javaType.build()).build() + + return CodeGenResult(javaDataFetchers = listOf(javaFile)) + } + + private fun createDataFetcherInterfaceMethodForOperation(field: FieldDefinition, parent: ObjectTypeDefinition): MethodSpec { + val returnType = TypeUtils(config.packageNameTypes, config, document).findReturnType(field.type) + + val methodSpec = MethodSpec.methodBuilder(field.name) + .returns( + if (parent.name == OperationTypes.subscription) + ParameterizedTypeName.get(ClassName.get(Publisher::class.java), returnType) + else + returnType + ) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addAnnotation( + when (parent.name) { + OperationTypes.query -> AnnotationSpec + .builder(DgsQuery::class.java) + .addMember(DgsQuery::field.name, "\$S", field.name) + .build() + OperationTypes.mutation -> AnnotationSpec + .builder(DgsMutation::class.java) + .addMember(DgsMutation::field.name, "\$S", field.name) + .build() + OperationTypes.subscription -> AnnotationSpec + .builder(DgsSubscription::class.java) + .addMember(DgsSubscription::field.name, "\$S", field.name) + .build() + else -> throw UnknownOperationException(parent.name) + } + ) + .addParameter(ParameterSpec.builder(DgsDataFetchingEnvironment::class.java, "dataFetchingEnvironment").build()) + + generateInputParameter(field.inputValueDefinitions).forEach(methodSpec::addParameter) + + return methodSpec.build() + } + + private fun createDataFetcherInterfaceMethodForType(field: FieldDefinition, parent: ObjectTypeDefinition): MethodSpec { + val returnType = TypeUtils(config.packageNameTypes, config, document).findReturnType(field.type) + + val methodSpec = MethodSpec.methodBuilder(field.name) + .returns(ParameterizedTypeName.get(ClassName.get(CompletableFuture::class.java), returnType)) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addAnnotation( + AnnotationSpec.builder(DgsData::class.java) + .addMember(DgsData::field.name, "\$S", field.name) + .addMember(DgsData::parentType.name, "\$S", parent.name) + .build() + ) + .addParameter( + ParameterSpec.builder(DgsDataFetchingEnvironment::class.java, "dataFetchingEnvironment").build() + ) + + generateInputParameter(field.inputValueDefinitions).forEach(methodSpec::addParameter) + + return methodSpec.build() + } + + private fun generateInputParameter(inputValueDefinitions: List): List { + return inputValueDefinitions.map { + ParameterSpec.builder( + TypeUtils(config.packageNameTypes, config, document).findReturnType(it.type), + it.name + ).addAnnotation( + AnnotationSpec.builder(InputArgument::class.java) + .addMember(InputArgument::name.name, "\$S", it.name) + .build() + ).build() + } + } + + private fun getPackageName(): String { + return config.packageNameDatafetchers + } +} + +private fun Type<*>.isID(): Boolean { + return when (this) { + is TypeName -> this.name == "ID" + is NonNullType -> this.type.isID() + is ListType -> false + else -> false + } +} \ No newline at end of file diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataFetcherInterfaceGeneratorTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataFetcherInterfaceGeneratorTest.kt new file mode 100644 index 000000000..1b78d028d --- /dev/null +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataFetcherInterfaceGeneratorTest.kt @@ -0,0 +1,238 @@ +/* + * + * 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.java + +import com.netflix.graphql.dgs.* +import com.netflix.graphql.dgs.codegen.* +import com.squareup.javapoet.AnnotationSpec +import com.squareup.javapoet.ClassName +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.MethodSpec +import com.squareup.javapoet.ParameterizedTypeName +import com.squareup.javapoet.TypeSpec.Kind +import org.assertj.core.api.Assertions.* +import org.junit.jupiter.api.Test +import org.reactivestreams.Publisher +import java.util.concurrent.CompletableFuture +import javax.lang.model.element.Modifier +import kotlin.reflect.KClass + +internal class DataFetcherInterfaceGeneratorTest { + + @Test + fun generateDataFetcherInterfaceForQuery() { + // GIVEN + val schema = """ + type Query { + people: [Person] + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + // WHEN + val codeGenResult = generateCode(schema) + val dataFetchers = codeGenResult.javaDataFetchers + val dataTypes = codeGenResult.javaDataTypes + + // THEN + assertThat(dataFetchers.size).isEqualTo(1) + + checkDataFetcherInterface( + javaFile = dataFetchers[0], + name = "QueryDataFetcher" + ) + + val method = dataFetchers[0].typeSpec.methodSpecs.first() + + checkInterfaceMethod(method, "people", java.util.List::class, "Person") + checkFirstParameterIsDataFetchingEnvironment(method) + checkAnnotation(method.annotations[0], DgsQuery::class, "field" to "people") + + assertCompilesJava(dataFetchers + dataTypes) + } + + @Test + fun generateDataFetcherInterfaceForMutation() { + // GIVEN + val schema = """ + type Mutation { + createPerson(person: Person): Boolean + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + // WHEN + val codeGenResult = generateCode(schema) + val dataFetchers = codeGenResult.javaDataFetchers + val dataTypes = codeGenResult.javaDataTypes + + // THEN + assertThat(dataFetchers.size).isEqualTo(1) + + checkDataFetcherInterface( + javaFile = dataFetchers[0], + name = "MutationDataFetcher" + ) + + val method = dataFetchers[0].typeSpec.methodSpecs.first() + checkInterfaceMethod(method, "createPerson", java.lang.Boolean::class) + checkFirstParameterIsDataFetchingEnvironment(method) + + assertThat(method.parameters).hasSize(2) + assertThat(method.parameters[1].name).isEqualTo("person") + assertThat(method.parameters[1].type).isEqualTo(ClassName.get("$basePackageName.types", "Person")) + assertThat(method.parameters[1].annotations).hasSize(1) + checkAnnotation(method.parameters[1].annotations[0], InputArgument::class, "name" to "person") + + checkAnnotation(method.annotations[0], DgsMutation::class, "field" to "createPerson") + + assertCompilesJava(dataFetchers + dataTypes) + } + + @Test + fun generateDataFetcherInterfaceForSubscription() { + // GIVEN + val schema = """ + type Subscription { + onPersonCreated: Person + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + // WHEN + val codeGenResult = generateCode(schema) + val dataFetchers = codeGenResult.javaDataFetchers + val dataTypes = codeGenResult.javaDataTypes + + // THEN + assertThat(dataFetchers.size).isEqualTo(1) + + checkDataFetcherInterface( + javaFile = dataFetchers[0], + name = "SubscriptionDataFetcher" + ) + + val method = dataFetchers[0].typeSpec.methodSpecs.first() + checkInterfaceMethod(method, "onPersonCreated", Publisher::class, "Person") + checkAnnotation(method.annotations[0], DgsSubscription::class, "field" to "onPersonCreated") + + assertThat(method.parameters).hasSize(1) + assertThat(method.parameters[0].name).isEqualTo("dataFetchingEnvironment") + assertThat(method.parameters[0].type).isEqualTo(ClassName.get(DgsDataFetchingEnvironment::class.java)) + + assertCompilesJava(dataFetchers + dataTypes) + } + + @Test + fun generateDataFetcherInterfaceForType() { + // GIVEN + val schema = """ + type Person { + id: ID! + firstname: String + lastname: String + partner: Person + } + """.trimIndent() + + // WHEN + val codeGenResult = generateCode(schema) + val dataFetchers = codeGenResult.javaDataFetchers + val dataTypes = codeGenResult.javaDataTypes + + // THEN + assertThat(dataFetchers.size).isEqualTo(1) + + checkDataFetcherInterface( + javaFile = dataFetchers[0], + name = "PersonDataFetcher" + ) + + val method = dataFetchers[0].typeSpec.methodSpecs.first() + checkInterfaceMethod(method, "partner", CompletableFuture::class, "Person") + checkAnnotation(method.annotations[0], DgsData::class, "field" to "partner", "parentType" to "Person") + checkFirstParameterIsDataFetchingEnvironment(method) + + assertCompilesJava(dataFetchers + dataTypes) + } + + private fun generateCode(schema: String) = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateDataFetchersAsInterfaces = true + ) + ).generate() + + private fun checkInterfaceMethod(method: MethodSpec, name: String, clazz: KClass<*>, genericType: String? = null) { + assertThat(method.name).isEqualTo(name) + assertThat(method.code.isEmpty).isTrue + assertThat(method.modifiers).contains(Modifier.ABSTRACT) + assertThat(method.annotations).hasSize(1) + + if (genericType != null) { + assertThat(method.returnType).isInstanceOf(ParameterizedTypeName::class.java) + assertThat((method.returnType as ParameterizedTypeName).rawType).isEqualTo(ClassName.get(clazz.java)) + assertThat((method.returnType as ParameterizedTypeName).typeArguments).contains( + ClassName.get( + "$basePackageName.types", + genericType + ) + ) + } else { + assertThat(method.returnType).isEqualTo(ClassName.get(clazz.java)) + } + } + + private fun checkDataFetcherInterface(javaFile: JavaFile, name: String) { + assertThat(javaFile.typeSpec.name).isEqualTo(name) + assertThat(javaFile.packageName).isEqualTo(dataFetcherPackageName) + assertThat(javaFile.typeSpec.methodSpecs).hasSize(1) + assertThat(javaFile.typeSpec.kind).isEqualTo(Kind.INTERFACE) + } + + private fun checkFirstParameterIsDataFetchingEnvironment(method: MethodSpec) { + assertThat(method.parameters).hasSizeGreaterThanOrEqualTo(1) + assertThat(method.parameters[0].name).isEqualTo("dataFetchingEnvironment") + assertThat(method.parameters[0].type).isEqualTo(ClassName.get(DgsDataFetchingEnvironment::class.java)) + } + + private fun checkAnnotation(annotation: AnnotationSpec, clazz: KClass<*>, vararg members: Pair) { + assertThat(annotation.type).isEqualTo(ClassName.get(clazz.java)) + assertThat(annotation.members).hasSize(members.size) + members.forEach { + assertThat(annotation.members).containsKey(it.first) + assertThat(annotation.members[it.first]).hasSize(1) + assertThat(annotation.members[it.first]?.first().toString()).isEqualTo("\"${it.second}\"") + } + } + +} \ No newline at end of file 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 e6e050063..dfa8ab410 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 @@ -78,6 +78,9 @@ open class GenerateJavaTask : DefaultTask() { @Input var generateInterfaceSetters = true + @Input + var generateDataFetchersAsInterfaces = false + @OutputDirectory fun getOutputDir(): File { return Paths.get("$generatedSourcesDir/generated/sources/dgs-codegen").toFile() @@ -141,6 +144,7 @@ open class GenerateJavaTask : DefaultTask() { generateClientApi = generateClient, generateInterfaces = generateInterfaces, generateInterfaceSetters = generateInterfaceSetters, + generateDataFetchersAsInterfaces = generateDataFetchersAsInterfaces, typeMapping = typeMapping, includeQueries = includeQueries.toSet(), includeMutations = includeMutations.toSet(), From adf5e890dc75e33f3de5d54122aaaa8d54173e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Carl=C3=A9?= Date: Mon, 14 Feb 2022 01:15:27 +0100 Subject: [PATCH 3/3] feature(kotlin): add generator for data fetcher interfaces --- .../netflix/graphql/dgs/codegen/CodeGen.kt | 13 +- .../java/DataFetcherInterfaceGenerator.kt | 22 +- .../KotlinDataFetcherInterfaceGenerator.kt | 152 +++++++++++ .../generators/shared/SharedTypeUtils.kt | 14 + .../netflix/graphql/dgs/codegen/TestUtils.kt | 1 + ...KotlinDataFetcherInterfaceGeneratorTest.kt | 242 ++++++++++++++++++ 6 files changed, 422 insertions(+), 22 deletions(-) create mode 100644 graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherInterfaceGenerator.kt create mode 100644 graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherInterfaceGeneratorTest.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 3d949a139..6274bdf1b 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 @@ -269,13 +269,22 @@ class CodeGen(private val config: CodeGenConfig) { } .fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) } + val dataFetcherResult = if (config.generateDataFetchersAsInterfaces) { + definitions.asSequence() + .filterIsInstance() + .map { KotlinDataFetcherInterfaceGenerator(config).generate(it) } + .fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) } + } else { + CodeGenResult() + } + val constantsClass = KotlinConstantsGenerator(config, document).generate() val client = generateJavaClientApi(definitions) val entitiesClient = generateJavaClientEntitiesApi(definitions) val entitiesRepresentationsTypes = generateKotlinClientEntitiesRepresentations(definitions) - return datatypesResult.merge(inputTypes).merge(interfacesResult).merge(unionResult).merge(enumsResult) + return datatypesResult.merge(inputTypes).merge(interfacesResult).merge(unionResult).merge(enumsResult).merge(dataFetcherResult) .merge(client).merge(entitiesClient).merge(entitiesRepresentationsTypes).merge(constantsClass) } @@ -306,7 +315,7 @@ class CodeGen(private val config: CodeGenConfig) { return definitions.asSequence() .filterIsInstance() .excludeSchemaTypeExtension() - .filter { it.name != OperationTypes.query && it.name != OperationTypes.mutation && it.name != "RelayPageInfo" } + .filter { !OperationTypes.isOperationType(it.name) && it.name != "RelayPageInfo" } .filter { config.generateDataTypes || it.name in requiredTypeCollector.requiredTypes } .map { val extensions = findTypeExtensions(it.name, definitions) diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataFetcherInterfaceGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataFetcherInterfaceGenerator.kt index d05ddd0ca..4fdb79417 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataFetcherInterfaceGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataFetcherInterfaceGenerator.kt @@ -23,23 +23,14 @@ import com.netflix.graphql.dgs.codegen.CodeGenConfig import com.netflix.graphql.dgs.codegen.CodeGenResult import com.netflix.graphql.dgs.codegen.OperationTypes import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized +import com.netflix.graphql.dgs.codegen.generators.shared.isID import com.netflix.graphql.dgs.codegen.isBaseType -import com.squareup.javapoet.AnnotationSpec -import com.squareup.javapoet.ClassName -import com.squareup.javapoet.JavaFile -import com.squareup.javapoet.MethodSpec -import com.squareup.javapoet.ParameterSpec -import com.squareup.javapoet.ParameterizedTypeName -import com.squareup.javapoet.TypeSpec +import com.squareup.javapoet.* import graphql.execution.UnknownOperationException import graphql.language.Document import graphql.language.FieldDefinition import graphql.language.InputValueDefinition -import graphql.language.ListType -import graphql.language.NonNullType import graphql.language.ObjectTypeDefinition -import graphql.language.Type -import graphql.language.TypeName import org.reactivestreams.Publisher import java.util.concurrent.CompletableFuture import javax.lang.model.element.Modifier @@ -157,12 +148,3 @@ class DataFetcherInterfaceGenerator(private val config: CodeGenConfig, private v return config.packageNameDatafetchers } } - -private fun Type<*>.isID(): Boolean { - return when (this) { - is TypeName -> this.name == "ID" - is NonNullType -> this.type.isID() - is ListType -> false - else -> false - } -} \ No newline at end of file diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherInterfaceGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherInterfaceGenerator.kt new file mode 100644 index 000000000..de4210dbe --- /dev/null +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherInterfaceGenerator.kt @@ -0,0 +1,152 @@ +/* + * + * 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.kotlin + +import com.netflix.graphql.dgs.* +import com.netflix.graphql.dgs.codegen.CodeGenConfig +import com.netflix.graphql.dgs.codegen.CodeGenResult +import com.netflix.graphql.dgs.codegen.OperationTypes +import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized +import com.netflix.graphql.dgs.codegen.generators.shared.isID +import com.netflix.graphql.dgs.codegen.isBaseType +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import graphql.execution.UnknownOperationException +import graphql.language.FieldDefinition +import graphql.language.InputValueDefinition +import graphql.language.ObjectTypeDefinition +import org.reactivestreams.Publisher +import java.util.concurrent.CompletableFuture + +class KotlinDataFetcherInterfaceGenerator(private val config: CodeGenConfig) { + fun generate(objectTypeDefinition: ObjectTypeDefinition): CodeGenResult { + val isOperationType = OperationTypes.isOperationType(objectTypeDefinition.name) + val fields = if (!isOperationType) { + objectTypeDefinition.fieldDefinitions.filter { !it.type.isBaseType() && !it.type.isID() } + } else { + objectTypeDefinition.fieldDefinitions + } + + if (fields.isNullOrEmpty()) { + return CodeGenResult() + } + + return createDataFetcherInterface( + objectTypeDefinition, + if (!isOperationType) { + fields.map { field -> + createDataFetcherInterfaceMethodForType(field, objectTypeDefinition) + } + } else { + fields.map { field -> + createDataFetcherInterfaceMethodForOperation(field, objectTypeDefinition) + } + } + ) + } + + private fun createDataFetcherInterface( + objectTypeDefinition: ObjectTypeDefinition, + methods: List + ): CodeGenResult { + val kotlinType = TypeSpec.interfaceBuilder(objectTypeDefinition.name.capitalized() + "DataFetcher") + .addModifiers(KModifier.PUBLIC) + .addFunctions(methods) + .build() + + val kotlinFile = FileSpec.builder(getPackageName(), objectTypeDefinition.name.capitalized() + "DataFetcher") + .addType(kotlinType) + .build() + + return CodeGenResult(kotlinDataFetchers = listOf(kotlinFile)) + } + + private fun createDataFetcherInterfaceMethodForOperation(field: FieldDefinition, parent: ObjectTypeDefinition): FunSpec { + val returnType = KotlinTypeUtils(config.packageNameTypes, config).findReturnType(field.type) + + val methodSpec = FunSpec.builder(field.name) + .returns( + if (parent.name == OperationTypes.subscription) + Publisher::class.asTypeName().parameterizedBy(returnType) + else + returnType + ) + .addModifiers(KModifier.PUBLIC, KModifier.ABSTRACT) + .addAnnotation( + when (parent.name) { + OperationTypes.query -> AnnotationSpec + .builder(DgsQuery::class) + .addMember("%L = %S", DgsQuery::field.name, field.name) + .build() + OperationTypes.mutation -> AnnotationSpec + .builder(DgsMutation::class) + .addMember("%L = %S", DgsMutation::field.name, field.name) + .build() + OperationTypes.subscription -> AnnotationSpec + .builder(DgsSubscription::class) + .addMember("%L = %S", DgsSubscription::field.name, field.name) + .build() + else -> throw UnknownOperationException(parent.name) + } + ) + .addParameter(ParameterSpec.builder("dataFetchingEnvironment", DgsDataFetchingEnvironment::class).build()) + + generateInputParameter(field.inputValueDefinitions).forEach(methodSpec::addParameter) + + return methodSpec.build() + } + + private fun createDataFetcherInterfaceMethodForType(field: FieldDefinition, parent: ObjectTypeDefinition): FunSpec { + val returnType = KotlinTypeUtils(config.packageNameTypes, config).findReturnType(field.type) + + val methodSpec = FunSpec.builder(field.name) + .returns(CompletableFuture::class.asTypeName().parameterizedBy(returnType)) + .addModifiers(KModifier.PUBLIC, KModifier.ABSTRACT) + .addAnnotation( + AnnotationSpec.builder(DgsData::class) + .addMember("%L = %S", DgsData::field.name, field.name) + .addMember("%L = %S", DgsData::parentType.name, parent.name) + .build() + ) + .addParameter( + ParameterSpec.builder("dataFetchingEnvironment", DgsDataFetchingEnvironment::class).build() + ) + + generateInputParameter(field.inputValueDefinitions).forEach(methodSpec::addParameter) + + return methodSpec.build() + } + + private fun generateInputParameter(inputValueDefinitions: List): List { + return inputValueDefinitions.map { + ParameterSpec.builder( + it.name, + KotlinTypeUtils(config.packageNameTypes, config).findReturnType(it.type) + ).addAnnotation( + AnnotationSpec.builder(InputArgument::class) + .addMember("%L = %S", InputArgument::name.name, it.name) + .build() + ).build() + } + } + + private fun getPackageName(): String { + return config.packageNameDatafetchers + } +} \ No newline at end of file diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/shared/SharedTypeUtils.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/shared/SharedTypeUtils.kt index afbe29c55..071c60fad 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/shared/SharedTypeUtils.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/shared/SharedTypeUtils.kt @@ -18,6 +18,11 @@ package com.netflix.graphql.dgs.codegen.generators.shared +import graphql.language.ListType +import graphql.language.NonNullType +import graphql.language.Type +import graphql.language.TypeName + internal sealed class GenericSymbol(open val index: Int) { class OpenBracket(str: String, startFrom: Int = 0) : GenericSymbol(str.indexOf("<", startFrom)) class CloseBracket(str: String, startFrom: Int = 0) : GenericSymbol(str.indexOf(">", startFrom)) @@ -120,3 +125,12 @@ internal fun parseMappedType( if (stack.isNotEmpty()) throw IllegalArgumentException("Wrong mapped type $mappedType") return mappedType.toTypeName(true) } + +fun Type<*>.isID(): Boolean { + return when (this) { + is TypeName -> this.name == "ID" + is NonNullType -> this.type.isID() + is ListType -> false + else -> false + } +} \ No newline at end of file diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/TestUtils.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/TestUtils.kt index 6d9bbc840..0b6b574d7 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/TestUtils.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/TestUtils.kt @@ -56,6 +56,7 @@ fun assertCompilesJava(javaFiles: Collection): Compilation { } fun assertCompilesKotlin(files: Collection): Path { + println(files.map { it.toString() }) val srcDir = Files.createTempDirectory("src") val buildDir = Files.createTempDirectory("build") files.forEach { it.writeTo(srcDir) } diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherInterfaceGeneratorTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherInterfaceGeneratorTest.kt new file mode 100644 index 000000000..e138955cc --- /dev/null +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinDataFetcherInterfaceGeneratorTest.kt @@ -0,0 +1,242 @@ +/* + * + * 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.kotlin + +import com.netflix.graphql.dgs.* +import com.netflix.graphql.dgs.codegen.* +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.TypeSpec.Kind +import org.assertj.core.api.Assertions.* +import org.junit.jupiter.api.Test +import org.reactivestreams.Publisher +import java.util.concurrent.CompletableFuture +import kotlin.reflect.KClass + +internal class KotlinDataFetcherInterfaceGeneratorTest { + + @Test + fun generateDataFetcherInterfaceForQuery() { + // GIVEN + val schema = """ + type Query { + people: [Person] + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + // WHEN + val codeGenResult = generateCode(schema) + val dataFetchers = codeGenResult.kotlinDataFetchers + val dataTypes = codeGenResult.kotlinDataTypes + + // THEN + assertThat(dataTypes.size).isEqualTo(1) + assertThat(dataFetchers.size).isEqualTo(1) + + checkDataFetcherInterface( + javaFile = dataFetchers[0], + name = "QueryDataFetcher" + ) + + val method = dataFetchers[0].members.filterIsInstance()[0].funSpecs.first() + + checkInterfaceMethod(method, "people", java.util.List::class, "Person?") + checkFirstParameterIsDataFetchingEnvironment(method) + checkAnnotation(method.annotations[0], DgsQuery::class, "field" to "people") + + assertCompilesKotlin(dataFetchers + dataTypes) + } + + @Test + fun generateDataFetcherInterfaceForMutation() { + // GIVEN + val schema = """ + type Mutation { + createPerson(person: Person): Boolean! + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + // WHEN + val codeGenResult = generateCode(schema) + val dataFetchers = codeGenResult.kotlinDataFetchers + val dataTypes = codeGenResult.kotlinDataTypes + + // THEN + assertThat(dataTypes.size).isEqualTo(1) + assertThat(dataFetchers.size).isEqualTo(1) + + checkDataFetcherInterface( + javaFile = dataFetchers[0], + name = "MutationDataFetcher" + ) + + assertThat(dataFetchers[0].members.filterIsInstance()).hasSize(1) + + val method = dataFetchers[0].members.filterIsInstance()[0].funSpecs.first() + checkInterfaceMethod(method, "createPerson", Boolean::class) + checkFirstParameterIsDataFetchingEnvironment(method) + + assertThat(method.parameters).hasSize(2) + assertThat(method.parameters[1].name).isEqualTo("person") + assertThat(method.parameters[1].type).isEqualTo("$basePackageName.types.Person?".toKtTypeName()) + assertThat(method.parameters[1].annotations).hasSize(1) + checkAnnotation(method.parameters[1].annotations[0], InputArgument::class, "name" to "person") + + checkAnnotation(method.annotations[0], DgsMutation::class, "field" to "createPerson") + + assertCompilesKotlin(dataFetchers + dataTypes) + } + + @Test + fun generateDataFetcherInterfaceForSubscription() { + // GIVEN + val schema = """ + type Subscription { + onPersonCreated: Person + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + // WHEN + val codeGenResult = generateCode(schema) + val dataFetchers = codeGenResult.kotlinDataFetchers + val dataTypes = codeGenResult.kotlinDataTypes + + // THEN + assertThat(dataTypes.size).isEqualTo(1) + assertThat(dataFetchers.size).isEqualTo(1) + + checkDataFetcherInterface( + javaFile = dataFetchers[0], + name = "SubscriptionDataFetcher" + ) + + assertThat(dataFetchers[0].members.filterIsInstance()).hasSize(1) + + val method = dataFetchers[0].members.filterIsInstance()[0].funSpecs.first() + checkInterfaceMethod(method, "onPersonCreated", Publisher::class, "Person?") + checkAnnotation(method.annotations[0], DgsSubscription::class, "field" to "onPersonCreated") + + assertThat(method.parameters).hasSize(1) + assertThat(method.parameters[0].name).isEqualTo("dataFetchingEnvironment") + assertThat(method.parameters[0].type).isEqualTo(DgsDataFetchingEnvironment::class.asTypeName()) + + assertCompilesKotlin(dataFetchers + dataTypes) + } + + @Test + fun generateDataFetcherInterfaceForType() { + // GIVEN + val schema = """ + type Person { + id: ID! + firstname: String + lastname: String + partner: Person + } + """.trimIndent() + + // WHEN + val codeGenResult = generateCode(schema) + val dataFetchers = codeGenResult.kotlinDataFetchers + val dataTypes = codeGenResult.kotlinDataTypes + + // THEN + assertThat(dataTypes.size).isEqualTo(1) + assertThat(dataFetchers.size).isEqualTo(1) + + checkDataFetcherInterface( + javaFile = dataFetchers[0], + name = "PersonDataFetcher" + ) + + assertThat(dataFetchers[0].members.filterIsInstance()).hasSize(1) + + val method = dataFetchers[0].members.filterIsInstance()[0].funSpecs.first() + checkInterfaceMethod(method, "partner", CompletableFuture::class, "Person?") + checkAnnotation(method.annotations[0], DgsData::class, "field" to "partner", "parentType" to "Person") + checkFirstParameterIsDataFetchingEnvironment(method) + + assertCompilesKotlin(dataFetchers + dataTypes) + } + + private fun generateCode(schema: String) = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + language = Language.KOTLIN, + generateDataFetchersAsInterfaces = true + ) + ).generate() + + private fun checkInterfaceMethod(method: FunSpec, name: String, clazz: KClass<*>, genericType: String? = null) { + assertThat(method.name).isEqualTo(name) + assertThat(method.body.isEmpty()).isTrue + assertThat(method.modifiers).contains(KModifier.ABSTRACT) + assertThat(method.annotations).hasSize(1) + + if (genericType != null) { + assertThat(method.returnType).isInstanceOf(ParameterizedTypeName::class.java) + assertThat((method.returnType as ParameterizedTypeName).rawType).isEqualTo(clazz.asTypeName()) + assertThat((method.returnType as ParameterizedTypeName).typeArguments).contains( + "$basePackageName.types.$genericType".toKtTypeName() + ) + } else { + assertThat(method.returnType).isEqualTo(clazz.asTypeName()) + } + } + + private fun checkDataFetcherInterface(javaFile: FileSpec, name: String) { + assertThat(javaFile.packageName).isEqualTo(dataFetcherPackageName) + + assertThat(javaFile.members.filterIsInstance()).hasSize(1) + val typeSpec = javaFile.members.filterIsInstance().first() + assertThat(typeSpec.name).isEqualTo(name) + assertThat(typeSpec.funSpecs).hasSize(1) + assertThat(typeSpec.kind).isEqualTo(Kind.INTERFACE) + } + + private fun checkFirstParameterIsDataFetchingEnvironment(method: FunSpec) { + assertThat(method.parameters).hasSizeGreaterThanOrEqualTo(1) + assertThat(method.parameters[0].name).isEqualTo("dataFetchingEnvironment") + assertThat(method.parameters[0].type).isEqualTo(DgsDataFetchingEnvironment::class.asTypeName()) + } + + private fun checkAnnotation(annotation: AnnotationSpec, clazz: KClass<*>, vararg pairs: Pair) { + assertThat(annotation.typeName).isEqualTo(clazz.asTypeName()) + assertThat(annotation.members).hasSize(pairs.size) + pairs.forEach { + assertThat(annotation.members).contains(CodeBlock.of("%L = %S", it.first, it.second)) + } + } + +} \ No newline at end of file