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 e4f26bfbd..64a1b229e 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 @@ -226,28 +226,38 @@ class CodeGen(private val config: CodeGenConfig) { private fun generateJavaClientApi(definitions: Collection>): CodeGenResult { val methodNames = mutableSetOf() - return if (config.generateClientApi) { + return if (config.generateClientApi || config.generateClientApiv2) { definitions.asSequence() .filterIsInstance() .filter { it.name == "Query" || it.name == "Mutation" || it.name == "Subscription" } .sortedBy { it.name.length } - .map { ClientApiGenerator(config, document).generate(it, methodNames) } + .map { + if (config.generateClientApiv2) { + ClientApiGeneratorv2(config, document).generate(it, methodNames) + } else { + ClientApiGenerator(config, document).generate(it, methodNames) + } + } .fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) } } else CodeGenResult() } private fun generateJavaClientEntitiesApi(definitions: Collection>): CodeGenResult { - return if (config.generateClientApi) { + return if (config.generateClientApi || config.generateClientApiv2) { val federatedDefinitions = definitions.asSequence() .filterIsInstance() .filter { it.hasDirective("key") } .toList() - ClientApiGenerator(config, document).generateEntities(federatedDefinitions) + if (config.generateClientApiv2) { + ClientApiGeneratorv2(config, document).generateEntities(federatedDefinitions) + } else { + ClientApiGenerator(config, document).generateEntities(federatedDefinitions) + } } else CodeGenResult() } private fun generateJavaClientEntitiesRepresentations(definitions: Collection>): CodeGenResult { - return if (config.generateClientApi) { + return if (config.generateClientApi || config.generateClientApiv2) { val generatedRepresentations = mutableMapOf() return definitions.asSequence() .filterIsInstance() @@ -366,7 +376,7 @@ class CodeGen(private val config: CodeGenConfig) { } else { val client = generateJavaClientApi(definitions) val entitiesClient = generateJavaClientEntitiesApi(definitions) - val entitiesRepresentationsTypes = generateKotlinClientEntitiesRepresentations(definitions) + val entitiesRepresentationsTypes = generateJavaClientEntitiesRepresentations(definitions) client.merge(entitiesClient).merge(entitiesRepresentationsTypes) } @@ -448,45 +458,46 @@ class CodeGen(private val config: CodeGenConfig) { } } -data class CodeGenConfig( - val schemas: Set = emptySet(), - val schemaFiles: Set = emptySet(), - val schemaJarFilesFromDependencies: List = emptyList(), - val outputDir: Path = Paths.get("generated"), - val examplesOutputDir: Path = Paths.get("generated-examples"), - val writeToFiles: Boolean = false, - val packageName: String = "com.netflix.${Paths.get("").toAbsolutePath().fileName}.generated", +class CodeGenConfig( + var schemas: Set = emptySet(), + var schemaFiles: Set = emptySet(), + var schemaJarFilesFromDependencies: List = emptyList(), + var outputDir: Path = Paths.get("generated"), + var examplesOutputDir: Path = Paths.get("generated-examples"), + var writeToFiles: Boolean = false, + var packageName: String = "com.netflix.${Paths.get("").toAbsolutePath().fileName}.generated", private val subPackageNameClient: String = "client", private val subPackageNameDatafetchers: String = "datafetchers", private val subPackageNameTypes: String = "types", - val language: Language = Language.JAVA, - val generateBoxedTypes: Boolean = false, - val generateClientApi: Boolean = false, - val generateInterfaces: Boolean = false, - val generateKotlinNullableClasses: Boolean = false, - val generateKotlinClosureProjections: Boolean = false, - val typeMapping: Map = emptyMap(), - val includeQueries: Set = emptySet(), - val includeMutations: Set = emptySet(), - val includeSubscriptions: Set = emptySet(), - val skipEntityQueries: Boolean = false, - val shortProjectionNames: Boolean = false, - val generateDataTypes: Boolean = true, - val omitNullInputFields: Boolean = false, - val maxProjectionDepth: Int = 10, - val kotlinAllFieldsOptional: Boolean = false, + var language: Language = Language.JAVA, + var generateBoxedTypes: Boolean = false, + var generateClientApi: Boolean = false, + var generateClientApiv2: Boolean = false, + var generateInterfaces: Boolean = false, + var generateKotlinNullableClasses: Boolean = false, + var generateKotlinClosureProjections: Boolean = false, + var typeMapping: Map = emptyMap(), + var includeQueries: Set = emptySet(), + var includeMutations: Set = emptySet(), + var includeSubscriptions: Set = emptySet(), + var skipEntityQueries: Boolean = false, + var shortProjectionNames: Boolean = false, + var generateDataTypes: Boolean = true, + var omitNullInputFields: Boolean = false, + var maxProjectionDepth: Int = 10, + var kotlinAllFieldsOptional: Boolean = false, /** 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 generateInterfaceMethodsForInterfaceFields: Boolean = false, - val includeImports: Map = emptyMap(), - val includeEnumImports: Map> = emptyMap(), - val includeClassImports: Map> = emptyMap(), - val generateCustomAnnotations: Boolean = false, + var snakeCaseConstantNames: Boolean = false, + var generateInterfaceSetters: Boolean = true, + var generateInterfaceMethodsForInterfaceFields: Boolean = false, + var includeImports: Map = emptyMap(), + var includeEnumImports: Map> = emptyMap(), + var includeClassImports: Map> = emptyMap(), + var generateCustomAnnotations: Boolean = false, var javaGenerateAllConstructor: Boolean = true, - val implementSerializable: Boolean = false, - val addGeneratedAnnotation: Boolean = false, - val addDeprecatedAnnotation: Boolean = false + var implementSerializable: Boolean = false, + var addGeneratedAnnotation: Boolean = false, + var addDeprecatedAnnotation: Boolean = false ) { val packageNameClient: String = "$packageName.$subPackageNameClient" @@ -544,7 +555,7 @@ data class CodeGenResult( val javaEnumTypes = this.javaEnumTypes.plus(current.javaEnumTypes) val javaDataFetchers = this.javaDataFetchers.plus(current.javaDataFetchers) val javaQueryTypes = this.javaQueryTypes.plus(current.javaQueryTypes) - val clientProjections = this.clientProjections.plus(current.clientProjections) + val clientProjections = this.clientProjections.plus(current.clientProjections).distinct() val javaConstants = this.javaConstants.plus(current.javaConstants) val kotlinDataTypes = this.kotlinDataTypes.plus(current.kotlinDataTypes) val kotlinInputTypes = this.kotlinInputTypes.plus(current.kotlinInputTypes) diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ClientApiGeneratorv2.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ClientApiGeneratorv2.kt new file mode 100644 index 000000000..5a77f5d35 --- /dev/null +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ClientApiGeneratorv2.kt @@ -0,0 +1,606 @@ +/* + * + * 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.client.codegen.BaseSubProjectionNode +import com.netflix.graphql.dgs.client.codegen.GraphQLQuery +import com.netflix.graphql.dgs.codegen.* +import com.netflix.graphql.dgs.codegen.generators.shared.ClassnameShortener +import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized +import com.squareup.javapoet.* +import graphql.introspection.Introspection.TypeNameMetaFieldDef +import graphql.language.* +import javax.lang.model.element.Modifier + +class ClientApiGeneratorv2(private val config: CodeGenConfig, private val document: Document) { + private val generatedClasses = mutableSetOf() + private val typeUtils = TypeUtils(getDatatypesPackageName(), config, document) + + fun generate(definition: ObjectTypeDefinition, methodNames: MutableSet): CodeGenResult { + return definition.fieldDefinitions.filterIncludedInConfig(definition.name, config).filterSkipped().map { + val javaFile = createQueryClass(it, definition.name, methodNames) + + val rootProjection = + it.type.findTypeDefinition(document, true)?.let { typeDefinition -> createRootProjection(typeDefinition, it.name.capitalized()) } + ?: CodeGenResult() + CodeGenResult(javaQueryTypes = listOf(javaFile)).merge(rootProjection) + }.fold(CodeGenResult()) { total, current -> total.merge(current) } + } + + fun generateEntities(definitions: List): CodeGenResult { + if (config.skipEntityQueries) { + return CodeGenResult() + } + + var entitiesRootProjection = CodeGenResult() + // generate for federation types, if present + val federatedTypes = definitions.filter { it.hasDirective("key") } + if (federatedTypes.isNotEmpty()) { + // create entities root projection + entitiesRootProjection = createEntitiesRootProjection(federatedTypes) + } + return CodeGenResult().merge(entitiesRootProjection) + } + + private fun createQueryClass(it: FieldDefinition, operation: String, methodNames: MutableSet): JavaFile { + val methodName = generateMethodName(it.name.capitalized(), operation.lowercase(), methodNames) + val javaType = TypeSpec.classBuilder(methodName) + .addOptionalGeneratedAnnotation(config) + .addModifiers(Modifier.PUBLIC).superclass(ClassName.get(GraphQLQuery::class.java)) + + if (it.description != null) { + javaType.addJavadoc(it.description.sanitizeJavaDoc()) + } + javaType.addMethod( + MethodSpec.methodBuilder("getOperationName") + .addModifiers(Modifier.PUBLIC) + .returns(String::class.java) + .addAnnotation(Override::class.java) + .addCode( + """ + | return "${it.name}"; + | + """.trimMargin() + ).build() + ) + + val setType = ClassName.get(Set::class.java) + val setOfStringType = ParameterizedTypeName.get(setType, ClassName.get(String::class.java)) + + val builderClass = TypeSpec.classBuilder("Builder").addModifiers(Modifier.STATIC, Modifier.PUBLIC) + .addOptionalGeneratedAnnotation(config) + .addMethod( + MethodSpec.methodBuilder("build") + .addModifiers(Modifier.PUBLIC) + .returns(ClassName.get("", methodName)) + .addCode( + if (it.inputValueDefinitions.isNotEmpty()) { + """ + |return new $methodName(${it.inputValueDefinitions.joinToString(", ") { ReservedKeywordSanitizer.sanitize(it.name) }}, queryName, fieldsSet); + | + """.trimMargin() + } else { + """ + |return new $methodName(queryName); + """.trimMargin() + } + ) + .build() + ).addField(FieldSpec.builder(setOfStringType, "fieldsSet", Modifier.PRIVATE).initializer("new \$T<>()", ClassName.get(HashSet::class.java)).build()) + + val constructorBuilder = MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + constructorBuilder.addCode( + """ + |super("${operation.lowercase()}", queryName); + | + """.trimMargin() + ) + + it.inputValueDefinitions.forEach { inputValue -> + val findReturnType = TypeUtils(getDatatypesPackageName(), config, document).findReturnType(inputValue.type) + val methodBuilder = MethodSpec.methodBuilder(ReservedKeywordSanitizer.sanitize(inputValue.name)) + .addParameter(findReturnType, ReservedKeywordSanitizer.sanitize(inputValue.name)) + .returns(ClassName.get("", "Builder")) + .addModifiers(Modifier.PUBLIC) + .addCode( + """ + |this.${ReservedKeywordSanitizer.sanitize(inputValue.name)} = ${ReservedKeywordSanitizer.sanitize(inputValue.name)}; + |this.fieldsSet.add("${inputValue.name}"); + |return this; + """.trimMargin() + ) + + if (inputValue.description != null) { + methodBuilder.addJavadoc(inputValue.description.sanitizeJavaDoc()) + } + builderClass.addMethod(methodBuilder.build()) + .addField(findReturnType, ReservedKeywordSanitizer.sanitize(inputValue.name), Modifier.PRIVATE) + + constructorBuilder.addParameter(findReturnType, ReservedKeywordSanitizer.sanitize(inputValue.name)) + + if (findReturnType.isPrimitive) { + constructorBuilder.addCode( + """ + |getInput().put("${inputValue.name}", ${ReservedKeywordSanitizer.sanitize(inputValue.name)}); + """.trimMargin() + ) + } else { + constructorBuilder.addCode( + """ + |if (${ReservedKeywordSanitizer.sanitize(inputValue.name)} != null || fieldsSet.contains("${inputValue.name}")) { + | getInput().put("${inputValue.name}", ${ReservedKeywordSanitizer.sanitize(inputValue.name)}); + |} + """.trimMargin() + ) + } + } + + val nameMethodBuilder = MethodSpec.methodBuilder("queryName") + .addParameter(String::class.java, "queryName") + .returns(ClassName.get("", "Builder")) + .addModifiers(Modifier.PUBLIC) + .addCode( + """ + |this.queryName = queryName; + |return this; + """.trimMargin() + ) + + builderClass.addField(FieldSpec.builder(String::class.java, "queryName", Modifier.PRIVATE).build()) + .addMethod(nameMethodBuilder.build()) + + constructorBuilder.addParameter(String::class.java, "queryName") + + if (it.inputValueDefinitions.size > 0) { + constructorBuilder.addParameter(setOfStringType, "fieldsSet") + } + + javaType.addMethod(constructorBuilder.build()) + + // No-arg constructor + javaType.addMethod( + MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC) + .addStatement("super(\"${operation.lowercase()}\")") + .build() + ) + + javaType.addMethod( + MethodSpec.methodBuilder("newRequest") + .addModifiers(Modifier.STATIC, Modifier.PUBLIC) + .returns(ClassName.get("", "Builder")) + .addCode("return new Builder();\n") + .build() + ) + javaType.addType(builderClass.build()) + return JavaFile.builder(getPackageName(), javaType.build()).build() + } + + /** + * Generate method name. If there are same method names in type `Query`, `Mutation` and `Subscription`, add suffix. + * For example, there are `shows` in `Query`, `Mutation` and `Subscription`, the generated files should be: + * `ShowsGraphQLQuery`, `ShowsGraphQLMutation` and `ShowsGraphQLSubscription` + */ + private fun generateMethodName(originalMethodName: String, typeName: String, methodNames: MutableSet): String { + return if ("mutation" == typeName && methodNames.contains(originalMethodName)) { + originalMethodName.plus("GraphQLMutation") + } else if ("subscription" == typeName && methodNames.contains(originalMethodName)) { + originalMethodName.plus("GraphQLSubscription") + } else { + methodNames.add(originalMethodName) + originalMethodName.plus("GraphQLQuery") + } + } + + private fun createRootProjection(type: TypeDefinition<*>, prefix: String): CodeGenResult { + val clazzName = "${prefix}ProjectionRoot" + val className = ClassName.get(BaseSubProjectionNode::class.java) + val parentJavaType = TypeVariableName.get("PARENT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?"))) + val rootJavaType = TypeVariableName.get("ROOT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?"))) + val javaType = TypeSpec.classBuilder(clazzName) + .addOptionalGeneratedAnnotation(config) + .addTypeVariable(parentJavaType) + .addTypeVariable(rootJavaType) + .addModifiers(Modifier.PUBLIC) + .superclass(ParameterizedTypeName.get(className, TypeVariableName.get("PARENT"), TypeVariableName.get("ROOT"))) + .addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addCode("""super(null, null, java.util.Optional.of("${type.name}"));""") + .build() + ) + + if (generatedClasses.contains(clazzName)) return CodeGenResult() else generatedClasses.add(clazzName) + + val fieldDefinitions = type.fieldDefinitions() + document.definitions.filterIsInstance().filter { it.name == type.name }.flatMap { it.fieldDefinitions } + + val codeGenResult = fieldDefinitions + .filterSkipped() + .mapNotNull { + val typeDefinition = it.type.findTypeDefinition( + document, + excludeExtensions = true, + includeBaseTypes = it.inputValueDefinitions.isNotEmpty(), + includeScalarTypes = it.inputValueDefinitions.isNotEmpty() + ) + if (typeDefinition != null) it to typeDefinition else null + } + .map { (fieldDef, typeDef) -> + val projectionName = "${typeDef.name.capitalized()}Projection" + if (typeDef !is ScalarTypeDefinition) { + val typeVariable = TypeVariableName.get("$projectionName<$clazzName, $clazzName>") + val noArgMethodBuilder = MethodSpec.methodBuilder(ReservedKeywordSanitizer.sanitize(fieldDef.name)) + .returns(typeVariable) + .addCode( + """ + |$projectionName<$clazzName, $clazzName> projection = new $projectionName<>(this, this); + |getFields().put("${fieldDef.name}", projection); + |return projection; + """.trimMargin() + ) + .addModifiers(Modifier.PUBLIC) + javaType.addMethod(noArgMethodBuilder.build()) + } + + if (fieldDef.inputValueDefinitions.isNotEmpty()) { + addFieldSelectionMethodWithArguments(fieldDef, projectionName, javaType, projectionRoot = "this") + } + + val processedEdges = mutableSetOf>() + processedEdges.add(typeDef.name to type.name) + createSubProjection(typeDef, javaType.build(), javaType.build(), "${typeDef.name.capitalized()}", processedEdges, 1) + } + .fold(CodeGenResult()) { total, current -> total.merge(current) } + + fieldDefinitions.filterSkipped().forEach { + val objectTypeDefinition = it.type.findTypeDefinition(document) + if (objectTypeDefinition == null) { + val typeVariable = TypeVariableName.get("$clazzName") + javaType.addMethod( + MethodSpec.methodBuilder(ReservedKeywordSanitizer.sanitize(it.name)) + .returns(typeVariable) + .addCode( + """ + |getFields().put("${it.name}", null); + |return this; + """.trimMargin() + ) + .addModifiers(Modifier.PUBLIC) + .build() + ) + } + } + + val concreteTypesResult = createConcreteTypes(type, javaType.build(), javaType, prefix, mutableSetOf(), 0) + val unionTypesResult = createUnionTypes(type, javaType, javaType.build(), prefix, mutableSetOf(), 0) + + val javaFile = JavaFile.builder(getPackageName(), javaType.build()).build() + return CodeGenResult(clientProjections = listOf(javaFile)).merge(codeGenResult).merge(concreteTypesResult).merge(unionTypesResult) + } + + private fun addFieldSelectionMethodWithArguments( + fieldDefinition: FieldDefinition, + projectionName: String, + javaType: TypeSpec.Builder, + projectionRoot: String + ): TypeSpec.Builder? { + val clazzName = javaType.build().name + val rootTypeName = if (projectionRoot == "this") "$clazzName" else "ROOT" + val returnTypeName = TypeVariableName.get("$projectionName<$clazzName, $rootTypeName>") + val methodBuilder = MethodSpec.methodBuilder(ReservedKeywordSanitizer.sanitize(fieldDefinition.name)) + .returns(returnTypeName) + .addCode( + """ + |$projectionName<$clazzName, $rootTypeName> projection = new $projectionName<>(this, $projectionRoot); + |getFields().put("${fieldDefinition.name}", projection); + |getInputArguments().computeIfAbsent("${fieldDefinition.name}", k -> new ${'$'}T<>()); + |${ + fieldDefinition.inputValueDefinitions.joinToString("\n") { input -> + """ + |InputArgument ${input.name}Arg = new InputArgument("${input.name}", ${input.name}); + |getInputArguments().get("${fieldDefinition.name}").add(${input.name}Arg); + """.trimMargin() + } + } + |return projection; + """.trimMargin(), + ArrayList::class.java + ) + .addModifiers(Modifier.PUBLIC) + + fieldDefinition.inputValueDefinitions.forEach { input -> + methodBuilder.addParameter(ParameterSpec.builder(typeUtils.findReturnType(input.type), input.name).build()) + } + return javaType.addMethod(methodBuilder.build()) + } + + private fun createEntitiesRootProjection(federatedTypes: List): CodeGenResult { + val clazzName = "EntitiesProjectionRoot" + val className = ClassName.get(BaseSubProjectionNode::class.java) + val parentType = TypeVariableName.get("PARENT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?"))) + val rootType = TypeVariableName.get("ROOT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?"))) + val javaType = TypeSpec.classBuilder(clazzName) + .addOptionalGeneratedAnnotation(config) + .addTypeVariable(parentType) + .addTypeVariable(rootType) + .addModifiers(Modifier.PUBLIC) + .superclass(ParameterizedTypeName.get(className, TypeVariableName.get("PARENT"), TypeVariableName.get("ROOT"))) + .addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addCode("""super(null, null, java.util.Optional.of("${"_entities"}"));""") + .build() + ) + + if (generatedClasses.contains(clazzName)) return CodeGenResult() else generatedClasses.add(clazzName) + + val codeGenResult = federatedTypes.map { objTypeDef -> + val projectionName = "Entities${objTypeDef.name.capitalized()}KeyProjection" + val returnType = TypeVariableName.get("$projectionName<$clazzName, $clazzName>") + javaType.addMethod( + MethodSpec.methodBuilder("on${objTypeDef.name}") + .addModifiers(Modifier.PUBLIC) + .returns(returnType) + .addCode( + """ + | Entities${objTypeDef.name.capitalized()}KeyProjection<$clazzName, $clazzName> fragment = new Entities${objTypeDef.name.capitalized()}KeyProjection(this, this); + | getFragments().add(fragment); + | return fragment; + """.trimMargin() + ) + .build() + ) + val processedEdges = mutableSetOf>() + createFragment(objTypeDef, javaType.build(), javaType.build(), "Entities${objTypeDef.name.capitalized()}Key", processedEdges, 0) + }.fold(CodeGenResult()) { total, current -> total.merge(current) } + + val javaFile = JavaFile.builder(getPackageName(), javaType.build()).build() + return CodeGenResult(clientProjections = listOf(javaFile)).merge(codeGenResult) + } + + private fun createConcreteTypes(type: TypeDefinition<*>, root: TypeSpec, javaType: TypeSpec.Builder, prefix: String, processedEdges: Set>, queryDepth: Int): CodeGenResult { + return if (type is InterfaceTypeDefinition) { + val concreteTypes = document.getDefinitionsOfType(ObjectTypeDefinition::class.java).filter { + it.implements.filterIsInstance>().any { iface -> iface.name == type.name } + } + concreteTypes.map { + addFragmentProjectionMethod(javaType, root, prefix, it, processedEdges, queryDepth) + }.fold(CodeGenResult()) { total, current -> total.merge(current) } + } else { + CodeGenResult() + } + } + + private fun createUnionTypes(type: TypeDefinition<*>, javaType: TypeSpec.Builder, rootType: TypeSpec, prefix: String, processedEdges: Set>, queryDepth: Int): CodeGenResult { + return if (type is UnionTypeDefinition) { + val memberTypes = type.memberTypes.mapNotNull { it.findTypeDefinition(document, true) }.toList() + memberTypes.map { + addFragmentProjectionMethod(javaType, rootType, prefix, it, processedEdges, queryDepth) + }.fold(CodeGenResult()) { total, current -> total.merge(current) } + } else { + CodeGenResult() + } + } + + private fun addFragmentProjectionMethod(javaType: TypeSpec.Builder, rootType: TypeSpec, prefix: String, it: TypeDefinition<*>, processedEdges: Set>, queryDepth: Int): CodeGenResult { + val rootRef = if (javaType.build().name == rootType.name) "this" else "getRoot()" + val rootTypeName = if (javaType.build().name == rootType.name) "${rootType.name}" else "ROOT" + val parentRef = javaType.build().name + val projectionName = "${it.name.capitalized()}Projection" + val typeVariable = TypeVariableName.get("$projectionName<$parentRef, $rootTypeName>") + javaType.addMethod( + MethodSpec.methodBuilder("on${it.name}") + .addModifiers(Modifier.PUBLIC) + .returns(typeVariable) + .addCode( + """ + |$projectionName<$parentRef, $rootTypeName> fragment = new $projectionName<>(this, $rootRef); + |getFragments().add(fragment); + |return fragment; + """.trimMargin() + ) + .build() + ) + + return createFragment(it as ObjectTypeDefinition, javaType.build(), rootType, "${it.name.capitalized()}", processedEdges, queryDepth) + } + + private fun createFragment(type: ObjectTypeDefinition, parent: TypeSpec, root: TypeSpec, prefix: String, processedEdges: Set>, queryDepth: Int): CodeGenResult { + val subProjection = createSubProjectionType(type, parent, root, prefix, processedEdges, queryDepth) + ?: return CodeGenResult() + val javaType = subProjection.first + val codeGenResult = subProjection.second + + // We don't need the typename added for fragments in the entities' projection. + // This affects deserialization when use directly with generated classes + if (prefix != "Entities${type.name.capitalized()}Key") { + javaType.addInitializerBlock( + CodeBlock.builder() + .addStatement("getFields().put(\$S, null)", TypeNameMetaFieldDef.name) + .build() + ) + } + + javaType.addMethod( + MethodSpec.methodBuilder("toString") + .returns(ClassName.get(String::class.java)) + .addAnnotation(Override::class.java) + .addModifiers(Modifier.PUBLIC) + .addCode( + """ + |StringBuilder builder = new StringBuilder(); + |builder.append("... on ${type.name} {"); + |getFields().forEach((k, v) -> { + | builder.append(" ").append(k); + | if(v != null) { + | builder.append(" ").append(v.toString()); + | } + |}); + |builder.append("}"); + | + |return builder.toString(); + """.trimMargin() + ) + .build() + ) + + val javaFile = JavaFile.builder(getPackageName(), javaType.build()).build() + return CodeGenResult(clientProjections = listOf(javaFile)).merge(codeGenResult) + } + + private fun createSubProjection(type: TypeDefinition<*>, parent: TypeSpec, root: TypeSpec, prefix: String, processedEdges: Set>, queryDepth: Int): CodeGenResult { + val subProjection = createSubProjectionType(type, parent, root, prefix, processedEdges, queryDepth) + ?: return CodeGenResult() + val javaType = subProjection.first + val codeGenResult = subProjection.second + + val javaFile = JavaFile.builder(getPackageName(), javaType.build()).build() + return CodeGenResult(clientProjections = listOf(javaFile)).merge(codeGenResult) + } + + private fun createSubProjectionType(type: TypeDefinition<*>, parent: TypeSpec, root: TypeSpec, prefix: String, processedEdges: Set>, queryDepth: Int): Pair? { + val className = ClassName.get(BaseSubProjectionNode::class.java) + val clazzName = "${prefix}Projection" + if (generatedClasses.contains(clazzName)) return null else generatedClasses.add(clazzName) + + val parentJavaType = TypeVariableName.get("PARENT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?"))) + val rootJavaType = TypeVariableName.get("ROOT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?"))) + val javaType = TypeSpec.classBuilder(clazzName) + .addOptionalGeneratedAnnotation(config) + .addTypeVariable(parentJavaType) + .addTypeVariable(rootJavaType) + .addModifiers(Modifier.PUBLIC) + .superclass(ParameterizedTypeName.get(className, TypeVariableName.get("PARENT"), TypeVariableName.get("ROOT"))) + .addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(ParameterSpec.builder(ClassName.get("", "PARENT"), "parent").build()) + .addParameter(ParameterSpec.builder(ClassName.get("", "ROOT"), "root").build()) + .addCode("""super(parent, root, java.util.Optional.of("${type.name}"));""") + .build() + ) + + val fieldDefinitions = type.fieldDefinitions() + + document.definitions + .filterIsInstance() + .filter { it.name == type.name } + .flatMap { it.fieldDefinitions } + + val codeGenResult = if (queryDepth < config.maxProjectionDepth || config.maxProjectionDepth == -1) { + fieldDefinitions + .filterSkipped() + .mapNotNull { + val typeDefinition = it.type.findTypeDefinition(document, true) + if (typeDefinition != null) it to typeDefinition else null + } + .map { (fieldDef, typeDef) -> + val projectionName = "${typeDef.name.capitalized()}Projection" + val methodName = ReservedKeywordSanitizer.sanitize(fieldDef.name) + val typeVariable = TypeVariableName.get("$projectionName<$clazzName, ROOT>") + javaType.addMethod( + MethodSpec.methodBuilder(methodName) + .returns(typeVariable) + .addCode( + """ + | $projectionName<$clazzName, ROOT> projection = new $projectionName<>(this, getRoot()); + | getFields().put("${fieldDef.name}", projection); + | return projection; + """.trimMargin() + ) + .addModifiers(Modifier.PUBLIC) + .build() + ) + + if (fieldDef.inputValueDefinitions.isNotEmpty()) { + addFieldSelectionMethodWithArguments(fieldDef, projectionName, javaType, projectionRoot = "getRoot()") + } + + val updatedProcessedEdges = processedEdges.toMutableSet() + updatedProcessedEdges.add(typeDef.name to type.name) + createSubProjection(typeDef, javaType.build(), root, "${typeDef.name.capitalized()}", updatedProcessedEdges, queryDepth + 1) + } + .fold(CodeGenResult()) { total, current -> total.merge(current) } + } else CodeGenResult() + + fieldDefinitions + .filterSkipped() + .forEach { + val objectTypeDefinition = it.type.findTypeDefinition(document) + if (objectTypeDefinition == null) { + val typeVariable = TypeVariableName.get("$clazzName") + javaType.addMethod( + MethodSpec.methodBuilder(ReservedKeywordSanitizer.sanitize(it.name)) + .returns(typeVariable) + .addCode( + """ + |getFields().put("${it.name}", null); + |return this; + """.trimMargin() + ) + .addModifiers(Modifier.PUBLIC) + .build() + ) + + if (it.inputValueDefinitions.isNotEmpty()) { + val methodWithInputArgumentsBuilder = MethodSpec.methodBuilder(ReservedKeywordSanitizer.sanitize(it.name)) + .returns(ClassName.get(getPackageName(), javaType.build().name)) + .addCode( + """ + |getFields().put("${it.name}", null); + |getInputArguments().computeIfAbsent("${it.name}", k -> new ${'$'}T<>()); + |${ + it.inputValueDefinitions.joinToString("\n") { input -> + """ + |InputArgument ${input.name}Arg = new InputArgument("${input.name}", ${input.name}); + |getInputArguments().get("${it.name}").add(${input.name}Arg); + """.trimMargin() + }} + |return this; + """.trimMargin(), + ArrayList::class.java + ) + .addModifiers(Modifier.PUBLIC) + + it.inputValueDefinitions.forEach { input -> + methodWithInputArgumentsBuilder.addParameter(ParameterSpec.builder(typeUtils.findReturnType(input.type), input.name).build()) + } + + javaType.addMethod(methodWithInputArgumentsBuilder.build()) + } + } + } + + val concreteTypesResult = createConcreteTypes(type, root, javaType, prefix, processedEdges, queryDepth) + val unionTypesResult = createUnionTypes(type, javaType, root, prefix, processedEdges, queryDepth) + + return javaType to codeGenResult.merge(concreteTypesResult).merge(unionTypesResult) + } + + private fun truncatePrefix(prefix: String): String { + return if (config.shortProjectionNames) ClassnameShortener.shorten(prefix) else prefix + } + + private fun getPackageName(): String { + return config.packageNameClient + } + + private fun getDatatypesPackageName(): String { + return config.packageNameTypes + } +} diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinEntitiesClientApiGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/EntitiesClientApiGenTestv2.kt similarity index 52% rename from graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinEntitiesClientApiGenTest.kt rename to graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/EntitiesClientApiGenTestv2.kt index 0e22dcde6..948ce0261 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinEntitiesClientApiGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/EntitiesClientApiGenTestv2.kt @@ -18,11 +18,13 @@ package com.netflix.graphql.dgs.codegen -import org.assertj.core.api.Assertions.* -import org.assertj.core.data.Index +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.fail +import org.junit.Ignore import org.junit.jupiter.api.Test -class KotlinEntitiesClientApiGenTest { +@Ignore +class EntitiesClientApiGenTestv2 { @Test fun `We can have federated entities`() { @@ -49,36 +51,18 @@ class KotlinEntitiesClientApiGenTest { val codeGenResult = codeGen(schema) - val projections = codeGenResult.clientProjections.filter { "Entities" in it.typeSpec.name } - assertThat(projections) - .hasSize(5) - .satisfies( - { file -> - assertThat(file.typeSpec.name).isEqualTo("EntitiesProjectionRoot") - assertThat(file.typeSpec.methodSpecs).extracting("name").containsExactly("onMovie") - }, - Index.atIndex(0) - ) - .satisfies( - { file -> assertThat(file.typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") }, - Index.atIndex(1) - ) - .satisfies( - { file -> assertThat(file.typeSpec.name).isEqualTo("EntitiesMovieKey_GenreProjection") }, - Index.atIndex(2) - ) - .satisfies( - { file -> assertThat(file.typeSpec.name).isEqualTo("EntitiesMovieKey_ActorProjection") }, - Index.atIndex(3) - ) - .satisfies( - { file -> assertThat(file.typeSpec.name).isEqualTo("EntitiesMovieKey_Actor_FriendsProjection") }, - Index.atIndex(4) - ) + val projections = codeGenResult.clientProjections + assertThat(projections[0].typeSpec.name).isEqualTo("EntitiesProjectionRoot") + assertThat(projections[0].typeSpec.methodSpecs).extracting("name").contains("onMovie") + assertThat(projections[1].typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") + assertThat(projections[2].typeSpec.name).isEqualTo("MovieGenreProjection") + assertThat(projections[3].typeSpec.name).isEqualTo("ActorProjection") - val representation = codeGenResult.kotlinDataTypes.single { "Representation" in it.name } + val representation = codeGenResult.javaDataTypes.single { "Representation" in it.typeSpec.name } + assertThat(representation.typeSpec.name).isEqualTo("MovieRepresentation") + assertThat(representation.typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "movieId") - assertThat(representation.name).isEqualTo("MovieRepresentation") codeGenResult.assertCompile() } @@ -103,15 +87,18 @@ class KotlinEntitiesClientApiGenTest { val codeGenResult = codeGen(schema) - val projections = codeGenResult.clientProjections.filter { "Entities" in it.typeSpec.name } - assertThat(projections[0].typeSpec.name).isEqualTo("EntitiesProjectionRoot") - assertThat(projections[0].typeSpec.methodSpecs).extracting("name").containsExactly("onMovie") - assertThat(projections[1].typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") - assertThat(projections[2].typeSpec.name).isEqualTo("EntitiesMovieKey_ActorProjection") - assertThat(projections[3].typeSpec.name).isEqualTo("EntitiesMovieKey_Actor_FriendsProjection") + val projections = codeGenResult.clientProjections + assertThat(projections[0].typeSpec.name).isEqualTo("SearchProjectionRoot") + assertThat(projections[1].typeSpec.name).isEqualTo("ActorProjection") + assertThat(projections[2].typeSpec.name).isEqualTo("EntitiesProjectionRoot") + assertThat(projections[2].typeSpec.methodSpecs).extracting("name").contains("onMovie") + assertThat(projections[3].typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") + + val representation = codeGenResult.javaDataTypes.single { "Representation" in it.typeSpec.name } + assertThat(representation.typeSpec.name).isEqualTo("MovieRepresentation") + assertThat(representation.typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "movieId") - val representation = codeGenResult.kotlinDataTypes.single { "Representation" in it.name } - assertThat(representation.name).isEqualTo("MovieRepresentation") codeGenResult.assertCompile() } @@ -140,18 +127,22 @@ class KotlinEntitiesClientApiGenTest { val codeGenResult = codeGen(schema) - val projections = codeGenResult.clientProjections.filter { "Entities" in it.typeSpec.name } - assertThat(projections[0].typeSpec.name).isEqualTo("EntitiesProjectionRoot") - assertThat(projections[0].typeSpec.methodSpecs).extracting("name").containsExactly("onMovie") - assertThat(projections[1].typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") - assertThat(projections[2].typeSpec.name).isEqualTo("EntitiesMovieKey_ActorProjection") - assertThat(projections[3].typeSpec.name).isEqualTo("EntitiesMovieKey_Actor_ActorProjection") - - val representations = codeGenResult.kotlinDataTypes.filter { "Representation" in it.name } - + val projections = codeGenResult.clientProjections + assertThat(projections[0].typeSpec.name).isEqualTo("SearchProjectionRoot") + assertThat(projections[1].typeSpec.name).isEqualTo("IActorProjection") + assertThat(projections[2].typeSpec.name).isEqualTo("ActorProjection") + assertThat(projections[3].typeSpec.name).isEqualTo("EntitiesProjectionRoot") + assertThat(projections[3].typeSpec.methodSpecs).extracting("name").contains("onMovie") + assertThat(projections[4].typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") + + val representations = codeGenResult.javaDataTypes.filter { "Representation" in it.typeSpec.name } assertThat(representations).hasSize(2) - assertThat(representations[0].name).isEqualTo("MovieRepresentation") - assertThat(representations[1].name).isEqualTo("IActorRepresentation") + assertThat(representations[0].typeSpec.name).isEqualTo("MovieRepresentation") + assertThat(representations[0].typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "actor") + assertThat(representations[1].typeSpec.name).isEqualTo("IActorRepresentation") + assertThat(representations[1].typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "name") codeGenResult.assertCompile() } @@ -176,15 +167,22 @@ class KotlinEntitiesClientApiGenTest { val codeGenResult = codeGen(schema) - val projections = codeGenResult.clientProjections.filter { "Entities" in it.typeSpec.name } - assertThat(projections[0].typeSpec.name).isEqualTo("EntitiesProjectionRoot") - assertThat(projections[0].typeSpec.methodSpecs).extracting("name").containsExactly("onMovie", "onActor") - assertThat(projections[1].typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") - assertThat(projections[2].typeSpec.name).isEqualTo("EntitiesMovieKey_ActorsProjection") - - val representations = codeGenResult.kotlinDataTypes.filter { "Representation" in it.name } + val projections = codeGenResult.clientProjections + assertThat(projections[0].typeSpec.name).isEqualTo("SearchProjectionRoot") + assertThat(projections[1].typeSpec.name).isEqualTo("ActorProjection") + assertThat(projections[2].typeSpec.name).isEqualTo("EntitiesProjectionRoot") + assertThat(projections[2].typeSpec.methodSpecs).extracting("name").contains("onMovie") + assertThat(projections[3].typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") + assertThat(projections[4].typeSpec.name).isEqualTo("EntitiesActorKeyProjection") + + val representations = codeGenResult.javaDataTypes.filter { "Representation" in it.typeSpec.name } assertThat(representations).hasSize(2) - assertThat(representations[0].name).isEqualTo("MovieRepresentation") + assertThat(representations[0].typeSpec.name).isEqualTo("MovieRepresentation") + assertThat(representations[0].typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "movieId", "actors") + assertThat(representations[0].typeSpec.fieldSpecs[1]).extracting("type") + .toString() + .contains("java.util.List") codeGenResult.assertCompile() } @@ -216,22 +214,24 @@ class KotlinEntitiesClientApiGenTest { val codeGenResult = codeGen(schema) - val projections = codeGenResult.clientProjections.filter { "Entities" in it.typeSpec.name } + val projections = codeGenResult.clientProjections.filter { it.typeSpec.name.startsWith("Entities") } assertThat(projections[0].typeSpec.name).isEqualTo("EntitiesProjectionRoot") assertThat(projections[0].typeSpec.methodSpecs).extracting("name") - .containsExactlyInAnyOrder("onMovie", "onMovieCast") + .containsExactlyInAnyOrder("", "onMovie", "onMovieCast") assertThat(projections[1].typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") - assertThat(projections[2].typeSpec.name).isEqualTo("EntitiesMovieKey_ActorProjection") - assertThat(projections[3].typeSpec.name).isEqualTo("EntitiesMovieCastKeyProjection") - assertThat(projections[4].typeSpec.name).isEqualTo("EntitiesMovieCastKey_MovieProjection") - assertThat(projections[5].typeSpec.name).isEqualTo("EntitiesMovieCastKey_Movie_ActorProjection") - assertThat(projections[6].typeSpec.name).isEqualTo("EntitiesMovieCastKey_ActorProjection") + assertThat(projections[2].typeSpec.name).isEqualTo("EntitiesMovieCastKeyProjection") - val representations = codeGenResult.kotlinDataTypes.filter { "Representation" in it.name } + val representations = codeGenResult.javaDataTypes.filter { "Representation" in it.typeSpec.name } assertThat(representations).hasSize(3) - assertThat(representations[0].name).isEqualTo("MovieRepresentation") - assertThat(representations[1].name).isEqualTo("PersonRepresentation") - assertThat(representations[2].name).isEqualTo("MovieCastRepresentation") + assertThat(representations[0].typeSpec.name).isEqualTo("MovieRepresentation") + assertThat(representations[0].typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "movieId", "actor") + assertThat(representations[1].typeSpec.name).isEqualTo("PersonRepresentation") + assertThat(representations[1].typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "name") + assertThat(representations[2].typeSpec.name).isEqualTo("MovieCastRepresentation") + assertThat(representations[2].typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "movie", "actor") codeGenResult.assertCompile() } @@ -261,15 +261,18 @@ class KotlinEntitiesClientApiGenTest { val projections = codeGenResult.clientProjections.filter { "Entities" in it.typeSpec.name } assertThat(projections[0].typeSpec.name).isEqualTo("EntitiesProjectionRoot") assertThat(projections[0].typeSpec.methodSpecs).extracting("name") - .containsExactlyInAnyOrder("onMovie", "onMovieActor") + .containsExactlyInAnyOrder("", "onMovie", "onMovieActor") assertThat(projections[1].typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") - assertThat(projections[2].typeSpec.name).isEqualTo("EntitiesMovieKey_ActorProjection") - assertThat(projections[3].typeSpec.name).isEqualTo("EntitiesMovieActorKeyProjection") + assertThat(projections[2].typeSpec.name).isEqualTo("EntitiesMovieActorKeyProjection") - val representations = codeGenResult.kotlinDataTypes.filter { "Representation" in it.name } + val representations = codeGenResult.javaDataTypes.filter { "Representation" in it.typeSpec.name } assertThat(representations).hasSize(2) - assertThat(representations[0].name).isEqualTo("MovieRepresentation") - assertThat(representations[1].name).isEqualTo("MovieActorRepresentation") + assertThat(representations[0].typeSpec.name).isEqualTo("MovieRepresentation") + assertThat(representations[0].typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "movieId") + assertThat(representations[1].typeSpec.name).isEqualTo("MovieActorRepresentation") + assertThat(representations[1].typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "name") codeGenResult.assertCompile() } @@ -296,28 +299,18 @@ class KotlinEntitiesClientApiGenTest { val codeGenResult = codeGen(schema) val projections = codeGenResult.clientProjections.filter { "Entities" in it.typeSpec.name } - assertThat(projections) - .hasSize(3) - .satisfies( - { file -> - assertThat(file.typeSpec.name).isEqualTo("EntitiesProjectionRoot") - assertThat(file.typeSpec.methodSpecs).extracting("name").containsExactly("onMovie") - }, - Index.atIndex(0) - ) - .satisfies( - { file -> assertThat(file.typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") }, - Index.atIndex(1) - ) - .satisfies( - { file -> assertThat(file.typeSpec.name).isEqualTo("EntitiesMovieKey_ActorProjection") }, - Index.atIndex(2) - ) + assertThat(projections[0].typeSpec.name).isEqualTo("EntitiesProjectionRoot") + assertThat(projections[0].typeSpec.methodSpecs).extracting("name").containsExactlyInAnyOrder("", "onMovie") + assertThat(projections[1].typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") - val representations = codeGenResult.kotlinDataTypes.filter { "Representation" in it.name } + val representations = codeGenResult.javaDataTypes.filter { "Representation" in it.typeSpec.name } assertThat(representations).hasSize(2) - assertThat(representations[0].name).isEqualTo("MovieRepresentation") - assertThat(representations[1].name).isEqualTo("PersonRepresentation") + assertThat(representations[0].typeSpec.name).isEqualTo("MovieRepresentation") + assertThat(representations[0].typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "movieId", "actor") + assertThat(representations[1].typeSpec.name).isEqualTo("PersonRepresentation") + assertThat(representations[1].typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "name", "age") codeGenResult.assertCompile() } @@ -347,14 +340,15 @@ class KotlinEntitiesClientApiGenTest { val projections = codeGenResult.clientProjections.filter { "Entities" in it.typeSpec.name } assertThat(projections[0].typeSpec.name).isEqualTo("EntitiesProjectionRoot") - assertThat(projections[0].typeSpec.methodSpecs).extracting("name").containsExactlyInAnyOrder("onMovie") + assertThat(projections[0].typeSpec.methodSpecs).extracting("name").containsExactlyInAnyOrder("", "onMovie") assertThat(projections[1].typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") - assertThat(projections[2].typeSpec.name).isEqualTo("EntitiesMovieKey_GenreProjection") - val representations = codeGenResult.kotlinDataTypes.filter { "Representation" in it.name } + val representations = codeGenResult.javaDataTypes.filter { "Representation" in it.typeSpec.name } assertThat(representations).hasSize(2) - assertThat(representations[0].name).isEqualTo("MovieRepresentation") - assertThat(representations[1].name).isEqualTo("MovieGenreRepresentation") + assertThat(representations[0].typeSpec.name).isEqualTo("MovieRepresentation") + assertThat(representations[0].typeSpec.fieldSpecs).extracting("name") + .containsExactlyInAnyOrder("__typename", "id", "genre") + assertThat(representations[1].typeSpec.name).isEqualTo("MovieGenreRepresentation") codeGenResult.assertCompile() } @@ -395,21 +389,38 @@ class KotlinEntitiesClientApiGenTest { val projections = codeGenResult.clientProjections.filter { "Entities" in it.typeSpec.name } assertThat(projections[0].typeSpec.name).isEqualTo("EntitiesProjectionRoot") - assertThat(projections[0].typeSpec.methodSpecs).extracting("name").containsExactlyInAnyOrder("onMovie") + assertThat(projections[0].typeSpec.methodSpecs).extracting("name").containsExactlyInAnyOrder("", "onMovie") assertThat(projections[1].typeSpec.name).isEqualTo("EntitiesMovieKeyProjection") - assertThat(projections[2].typeSpec.name).isEqualTo("EntitiesMovieKey_GenreProjection") - assertThat(projections[3].typeSpec.name).isEqualTo("EntitiesMovieKey_ActorProjection") - assertThat(projections[4].typeSpec.name).isEqualTo("EntitiesMovieKey_Actor_RoleProjection") - assertThat(projections[5].typeSpec.name).isEqualTo("EntitiesMovieKey_LocationProjection") - val representations = codeGenResult.kotlinDataTypes.filter { "Representation" in it.name } - assertThat(representations.map { it.name }) + val representations = codeGenResult.javaDataTypes.filter { "Representation" in it.typeSpec.name } + assertThat(representations.map { it.typeSpec.name }) .containsExactlyInAnyOrder( "MovieRepresentation", "MovieGenreRepresentation", "PersonRepresentation", "LocationRepresentation" ) + + assertThat(representations.first { it.typeSpec.name == "MovieRepresentation" }.typeSpec.fieldSpecs) + .extracting("name").containsExactlyInAnyOrder("__typename", "id", "actor", "genre", "location") + + val movieRepresentationType = representations.find { it.typeSpec.name == "MovieRepresentation" } + ?: fail("MovieRepresentation type not found") + assertThat(movieRepresentationType.typeSpec.fieldSpecs.map { it.name to it.type.toString() }) + .containsExactlyInAnyOrder( + "id" to "java.lang.String", + "genre" to "com.netflix.graphql.dgs.codegen.tests.generated.client.MovieGenreRepresentation", + "actor" to "com.netflix.graphql.dgs.codegen.tests.generated.client.PersonRepresentation", + "location" to "com.netflix.graphql.dgs.codegen.tests.generated.client.LocationRepresentation", + "__typename" to "java.lang.String" + ) + + assertThat(representations.first { it.typeSpec.name == "PersonRepresentation" }.typeSpec.fieldSpecs) + .extracting("name").containsExactlyInAnyOrder("__typename", "id") + + assertThat(representations.first { it.typeSpec.name == "LocationRepresentation" }.typeSpec.fieldSpecs) + .extracting("name").containsExactlyInAnyOrder("__typename", "id") + codeGenResult.assertCompile() } @@ -430,7 +441,7 @@ class KotlinEntitiesClientApiGenTest { val codeGenResult = codeGen(schema) - val representations = codeGenResult.kotlinDataTypes.filter { "Representation" in it.name } + val representations = codeGenResult.javaDataTypes.filter { "Representation" in it.typeSpec.name } assertThat(representations).hasSize(1) val projections = codeGenResult.clientProjections assertThat(projections).hasSize(3) @@ -462,8 +473,7 @@ class KotlinEntitiesClientApiGenTest { schemas = setOf(schema), packageName = basePackageName, generateClientApi = true, - skipEntityQueries = true, - language = Language.KOTLIN + skipEntityQueries = true ) ).generate() @@ -473,18 +483,82 @@ class KotlinEntitiesClientApiGenTest { codeGenResult.assertCompile() } + @Test + fun `Generate projections for the entities' keys`() { + val schema = """ + type Foo @key(fields:"id") { + id: ID + stringField: String + barField: Bar + mStringField(arg1: Int, arg2: String): [String!] + mBarField(arg1: Int, arg2: String): [Bar!] + } + + type Bar { + id: ID + baz: String + } + """.trimIndent() + + val codeGenResult = codeGen(schema) + // then + val testClassLoader = codeGenResult.assertCompile().toClassLoader() + // assert projection classes + val (entityRootProjectionClass, entitiesFooKeyProjectionClass, barProjectionClass) = + arrayOf( + "EntitiesProjectionRoot", + "EntitiesFooKeyProjection", + "BarProjection" + ).map { + val clazzCanonicalName = "$basePackageName.client.$it" + val clazz = testClassLoader.loadClass(clazzCanonicalName) + assertThat(clazz).describedAs(clazzCanonicalName).isNotNull + clazz + } + + // assert classes methods... + assertThat(entityRootProjectionClass).isNotNull.hasPublicMethods("onFoo") + + assertThat(entitiesFooKeyProjectionClass).isNotNull.hasPublicMethods( + "id", + "stringField", + "barField", + "mStringField", + "mBarField" + ) + // entitiesFooKeyProjectionClass methods + mapOf( + "id" to entitiesFooKeyProjectionClass, + "stringField" to entitiesFooKeyProjectionClass, + "barField" to barProjectionClass, + "mStringField" to entitiesFooKeyProjectionClass, + "mBarField" to barProjectionClass + ).forEach { (name, returnClass) -> + assertThat(entitiesFooKeyProjectionClass.getMethod(name)) + .describedAs("${entitiesFooKeyProjectionClass.name} method: $name").isNotNull.returns(returnClass) { it.returnType } + } + + mapOf( + "mBarField" to (arrayOf(Integer::class.java, String::class.java) to barProjectionClass), + "mStringField" to (arrayOf(Integer::class.java, String::class.java) to entitiesFooKeyProjectionClass) + ).forEach { (name, p) -> + val (_, returnClass) = p + assertThat(entitiesFooKeyProjectionClass.getMethod(name)) + .describedAs("method: $name").isNotNull.returns(returnClass) { it.returnType } + } + } + companion object { fun codeGen(schema: String): CodeGenResult { return CodeGen( CodeGenConfig( schemas = setOf(schema), packageName = basePackageName, - generateClientApi = true, - language = Language.KOTLIN + generateClientApiv2 = true, + language = Language.JAVA ) ).generate() } - - fun CodeGenResult.assertCompile() = assertCompilesKotlin(this) + fun CodeGenResult.assertCompile() = assertCompilesJava(this) } } 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 66af133c7..3a8ae85a8 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 @@ -2537,7 +2537,7 @@ class KotlinCodeGenTest { assertThat(dataTypes).hasSize(1) assertThat(dataTypes[0].name).isEqualTo("Person") - val annotationSpec = (((dataTypes as ArrayList<*>)[0] as FileSpec).members[0] as TypeSpec).annotationSpecs[0] + val annotationSpec = (dataTypes[0].members[0] as TypeSpec).annotationSpecs[0] assertThat((annotationSpec.typeName as ClassName).canonicalName).isEqualTo("com.test.validator.ValidPerson") assertThat(annotationSpec.members[0]).extracting("args").asList().hasSize(1) assertThat(annotationSpec.members[0]).extracting("args").asString().contains("com.enums.HUSBAND", "com.enums.WIFE") diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenBuilderTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenBuilderTest.kt index 9ee30a93b..6ab432a29 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenBuilderTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenBuilderTest.kt @@ -16,7 +16,7 @@ * */ -package com.netflix.graphql.dgs.codegen.clientapi +package com.netflix.graphql.dgs.codegen.clientapiv2 import com.netflix.graphql.dgs.client.codegen.GraphQLQuery import com.netflix.graphql.dgs.codegen.* diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenFragmentTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenFragmentTest.kt index dd92193c5..99bc41a72 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenFragmentTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenFragmentTest.kt @@ -16,7 +16,7 @@ * */ -package com.netflix.graphql.dgs.codegen.clientapi +package com.netflix.graphql.dgs.codegen.clientapiv2 import com.netflix.graphql.dgs.codegen.* import com.squareup.javapoet.JavaFile diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenMutationTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenMutationTest.kt index 6dbe79db6..a2b161ebf 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenMutationTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenMutationTest.kt @@ -16,7 +16,7 @@ * */ -package com.netflix.graphql.dgs.codegen.clientapi +package com.netflix.graphql.dgs.codegen.clientapiv2 import com.netflix.graphql.dgs.codegen.CodeGen import com.netflix.graphql.dgs.codegen.CodeGenConfig diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenProjectionTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenProjectionTest.kt index f8c5b25da..b4471979f 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenProjectionTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenProjectionTest.kt @@ -16,7 +16,7 @@ * */ -package com.netflix.graphql.dgs.codegen.clientapi +package com.netflix.graphql.dgs.codegen.clientapiv2 import com.netflix.graphql.dgs.codegen.CodeGen import com.netflix.graphql.dgs.codegen.CodeGenConfig diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenQueryTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenQueryTest.kt index 188ee6436..43dc21c06 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenQueryTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenQueryTest.kt @@ -16,7 +16,7 @@ * */ -package com.netflix.graphql.dgs.codegen.clientapi +package com.netflix.graphql.dgs.codegen.clientapiv2 import com.netflix.graphql.dgs.codegen.* import org.assertj.core.api.Assertions.assertThat diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenSubscriptionTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenSubscriptionTest.kt index dc4cb431b..e316cf928 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenSubscriptionTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenSubscriptionTest.kt @@ -16,7 +16,7 @@ * */ -package com.netflix.graphql.dgs.codegen.clientapi +package com.netflix.graphql.dgs.codegen.clientapiv2 import com.netflix.graphql.dgs.codegen.CodeGen import com.netflix.graphql.dgs.codegen.CodeGenConfig diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenBuilderTestv2.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenBuilderTestv2.kt new file mode 100644 index 000000000..b0e30bea3 --- /dev/null +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenBuilderTestv2.kt @@ -0,0 +1,141 @@ +/* + * + * 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.clientapiv2 + +import com.netflix.graphql.dgs.client.codegen.GraphQLQuery +import com.netflix.graphql.dgs.codegen.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class ClientApiGenBuilderTestv2 { + @Test + fun `Fields explicitly set to null in the builder should be included`() { + val schema = """ + type Query { + filter(nameFilter: String): [String] + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + maxProjectionDepth = 2 + ) + ).generate() + + val builderClass = assertCompilesJava(codeGenResult).toClassLoader() + .loadClass("$basePackageName.client.FilterGraphQLQuery\$Builder") + + val buildMethod = builderClass.getMethod("build") + val nameMethod = builderClass.getMethod("nameFilter", String::class.java) + + // When the 'nameFilter' method is invoked with a null value, the field should be included in the input map and explicitly set to null. + val builder1 = builderClass.constructors[0].newInstance() + nameMethod.invoke(builder1, null) + val resultQueryObject: GraphQLQuery = buildMethod.invoke(builder1) as GraphQLQuery + assertThat(resultQueryObject.input.keys).containsExactly("nameFilter") + assertThat(resultQueryObject.input["nameFilter"]).isNull() + } + + @Test + fun `Fields not explicitly set to null or any value in the builder should not be included`() { + val schema = """ + type Query { + filter(nameFilter: String): [String] + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + maxProjectionDepth = 2 + ) + ).generate() + + val builderClass = assertCompilesJava(codeGenResult).toClassLoader() + .loadClass("$basePackageName.client.FilterGraphQLQuery\$Builder") + val buildMethod = builderClass.getMethod("build") + + // When the 'nameFilter' method is not invoked, it should not be included in the input map. + val builder2 = builderClass.constructors[0].newInstance() + val result2QueryObject: GraphQLQuery = buildMethod.invoke(builder2) as GraphQLQuery + assertThat(result2QueryObject.input.keys).isEmpty() + assertThat(result2QueryObject.input["nameFilter"]).isNull() + } + + @Test + fun `Query name should be null if not set`() { + val schema = """ + type Query { + filter(nameFilter: String): [String] + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + maxProjectionDepth = 2 + ) + ).generate() + + val builderClass = assertCompilesJava(codeGenResult).toClassLoader() + .loadClass("$basePackageName.client.FilterGraphQLQuery\$Builder") + val buildMethod = builderClass.getMethod("build") + + val builder = builderClass.constructors[0].newInstance() + val result2QueryObject: GraphQLQuery = buildMethod.invoke(builder) as GraphQLQuery + assertThat(result2QueryObject.name).isNull() + } + + @Test + fun `Query name should be accessible via GraphQLQuery#name if set`() { + val schema = """ + type Query { + filter(nameFilter: String): [String] + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + maxProjectionDepth = 2 + ) + ).generate() + + val builderClass = assertCompilesJava(codeGenResult).toClassLoader() + .loadClass("$basePackageName.client.FilterGraphQLQuery\$Builder") + val nameMethod = builderClass.getMethod("queryName", String::class.java) + val buildMethod = builderClass.getMethod("build") + + val builder = builderClass.constructors[0].newInstance() + nameMethod.invoke(builder, "test") + + val result2QueryObject: GraphQLQuery = buildMethod.invoke(builder) as GraphQLQuery + assertThat(result2QueryObject.name).isNotNull + assertThat(result2QueryObject.name).isEqualTo("test") + } +} diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenFragmentTestv2.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenFragmentTestv2.kt new file mode 100644 index 000000000..480e5f7de --- /dev/null +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenFragmentTestv2.kt @@ -0,0 +1,249 @@ +/* + * + * 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.clientapiv2 + +import com.netflix.graphql.dgs.codegen.CodeGen +import com.netflix.graphql.dgs.codegen.CodeGenConfig +import com.netflix.graphql.dgs.codegen.assertCompilesJava +import com.netflix.graphql.dgs.codegen.basePackageName +import com.squareup.javapoet.JavaFile +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class ClientApiGenFragmentTestv2 { + @Test + fun interfaceFragment() { + val schema = """ + type Query { + search(title: String): [Show] + } + + interface Show { + title: String + } + + type Movie implements Show { + title: String + duration: Int + } + + type Series implements Show { + title: String + episodes: Int + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(3) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("SearchProjectionRoot") + assertThat(codeGenResult.clientProjections[0].typeSpec.methodSpecs).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("MovieProjection") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name").contains("duration") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name") + .doesNotContain("episodes") + assertThat(codeGenResult.clientProjections[2].typeSpec.name).isEqualTo("SeriesProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec.methodSpecs).extracting("name").contains("episodes") + assertThat(codeGenResult.clientProjections[2].typeSpec.methodSpecs).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[2].typeSpec.methodSpecs).extracting("name") + .doesNotContain("duration") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + codeGenResult.javaDataTypes + codeGenResult.javaInterfaces + ) + } + + @Test + fun interfaceFragmentOnSubType() { + val schema = """ + type Query { + search(title: String): [Result] + } + + type Result { + show: Show + } + + interface Show { + title: String + } + + type Movie implements Show { + title: String + duration: Int + } + + type Series implements Show { + title: String + episodes: Int + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(4) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("SearchProjectionRoot") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("ShowProjection") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[2].typeSpec.name).isEqualTo("MovieProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec.methodSpecs).extracting("name").contains("duration") + assertThat(codeGenResult.clientProjections[2].typeSpec.methodSpecs).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[2].typeSpec.methodSpecs).extracting("name") + .doesNotContain("episodes") + assertThat(codeGenResult.clientProjections[3].typeSpec.name).isEqualTo("SeriesProjection") + assertThat(codeGenResult.clientProjections[3].typeSpec.methodSpecs).extracting("name").contains("episodes") + assertThat(codeGenResult.clientProjections[3].typeSpec.methodSpecs).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[3].typeSpec.methodSpecs).extracting("name") + .doesNotContain("duration") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + codeGenResult.javaDataTypes + codeGenResult.javaInterfaces + ) + } + + @Test + fun unionFragment() { + val schema = """ + type Query { + search: [Result] + } + + union Result = Movie | Actor + + type Movie { + title: String + } + + type Actor { + name: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(3) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("SearchProjectionRoot") + assertThat(codeGenResult.clientProjections[0].typeSpec.methodSpecs).extracting("name").contains("onMovie") + assertThat(codeGenResult.clientProjections[0].typeSpec.methodSpecs).extracting("name").contains("onActor") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("MovieProjection") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name").doesNotContain("name") + assertThat(codeGenResult.clientProjections[2].typeSpec.name).isEqualTo("ActorProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec.methodSpecs).extracting("name").contains("name") + assertThat(codeGenResult.clientProjections[2].typeSpec.methodSpecs).extracting("name").doesNotContain("title") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + codeGenResult.javaDataTypes + codeGenResult.javaInterfaces + ) + } + + @Test + fun unionFragmentOnSubType() { + val schema = """ + type Query { + search(title: String): [Result] + } + + type Result { + result: SearchResult + } + + union SearchResult = Movie | Actor + + type Movie { + title: String + } + + type Actor { + name: String + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(4) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("SearchProjectionRoot") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("SearchResultProjection") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name").doesNotContain("title") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name").doesNotContain("name") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name").contains("onMovie") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name").contains("onActor") + assertThat(codeGenResult.clientProjections[2].typeSpec.name).isEqualTo("MovieProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec.methodSpecs).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[2].typeSpec.methodSpecs).extracting("name").doesNotContain("name") + assertThat(codeGenResult.clientProjections[3].typeSpec.name).isEqualTo("ActorProjection") + assertThat(codeGenResult.clientProjections[3].typeSpec.methodSpecs).extracting("name").contains("name") + assertThat(codeGenResult.clientProjections[3].typeSpec.methodSpecs).extracting("name").doesNotContain("title") + + assertThat(codeGenResult.clientProjections[2].typeSpec.initializerBlock.isEmpty).isFalse + assertThat(codeGenResult.clientProjections[3].typeSpec.initializerBlock.isEmpty).isFalse + + val searchResult = codeGenResult.javaInterfaces[0].typeSpec + + assertThat(JavaFile.builder("$basePackageName.types", searchResult).build().toString()).isEqualTo( + """ + |package com.netflix.graphql.dgs.codegen.tests.generated.types; + | + |import com.fasterxml.jackson.annotation.JsonSubTypes; + |import com.fasterxml.jackson.annotation.JsonTypeInfo; + | + |@JsonTypeInfo( + | use = JsonTypeInfo.Id.NAME, + | include = JsonTypeInfo.As.PROPERTY, + | property = "__typename" + |) + |@JsonSubTypes({ + | @JsonSubTypes.Type(value = Movie.class, name = "Movie"), + | @JsonSubTypes.Type(value = Actor.class, name = "Actor") + |}) + |public interface SearchResult { + |} + | + """.trimMargin() + ) + } +} diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenMutationTestv2.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenMutationTestv2.kt new file mode 100644 index 000000000..d690601fb --- /dev/null +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenMutationTestv2.kt @@ -0,0 +1,302 @@ +/* + * + * 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.clientapiv2 + +import com.netflix.graphql.dgs.codegen.CodeGen +import com.netflix.graphql.dgs.codegen.CodeGenConfig +import com.netflix.graphql.dgs.codegen.assertCompilesJava +import com.netflix.graphql.dgs.codegen.basePackageName +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class ClientApiGenMutationTestv2 { + @Test + fun generateMutationType() { + val schema = """ + type Mutation { + updateMovie(movieId: ID, title: String): Movie + } + + type Movie { + movieId: ID + title: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("UpdateMovieGraphQLQuery") + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes) + } + + @Test + fun generateMutationWithInputType() { + val schema = """ + type Mutation { + updateMovie(movie: MovieDescription): Movie + } + + input MovieDescription { + movieId: ID + title: String + actors: [String] + } + + type Movie { + movieId: ID + lastname: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("UpdateMovieGraphQLQuery") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaDataTypes + ) + } + + @Test + fun generateMutationWithInputDescription() { + val schema = """ + type Mutation { + updateMovie( + ""${'"'} + Some movie description + ""${'"'} + movie: MovieDescription): Movie + } + + input MovieDescription { + movieId: ID + title: String + actors: [String] + } + + type Movie { + movieId: ID + lastname: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.typeSpecs[0].methodSpecs[1].javadoc.toString()).isEqualTo("Some movie description") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaDataTypes + ) + } + + @Test + fun generateMutationAddsNullChecksDuringInit() { + val schema = """ + type Mutation { + updateMovie(movie: MovieDescription, reviews: [String], uuid: UUID): Movie + } + + input MovieDescription { + movieId: Int + title: String + actors: [String] + } + + type Movie { + movieId: Int + lastname: String + } + + scalar UUID @javaType(name : "java.util.UUID") + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + val initMethod = codeGenResult.javaQueryTypes[0].typeSpec.methodSpecs + .find { it.name == "" }?.code.toString() + + val expected = """ + |super("mutation", queryName); + |if (movie != null || fieldsSet.contains("movie")) { + | getInput().put("movie", movie); + |}if (reviews != null || fieldsSet.contains("reviews")) { + | getInput().put("reviews", reviews); + |}if (uuid != null || fieldsSet.contains("uuid")) { + | getInput().put("uuid", uuid); + |} + """.trimMargin() + + assert(initMethod.contains(expected)) + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaDataTypes + ) + } + + @Test + fun generateMutationDoesNotAddNullChecksForPrimitiveTypesDuringInit() { + val schema = """ + type Mutation { + updateMovie(movieId: Int!): Movie + } + + type Movie { + movieId: Int + lastname: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assert( + codeGenResult.javaQueryTypes[0].typeSpec.methodSpecs + .find { it.name == "" }?.code.toString() + .contains("super(\"mutation\", queryName);\ngetInput().put(\"movieId\", movieId);") + ) + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaDataTypes + ) + } + + @Test + fun generateOnlyRequiredDataTypesForMutation() { + val schema = """ + type Mutation { + shows(showFilter: ShowFilter): [Show] + people(personFilter: PersonFilter): [Person] + } + + type Show { + title: String + tags(from: Int, to: Int, sourceType: SourceType): [ShowTag] + isLive(countryFilter: CountryFilter): Boolean + } + + enum ShouldNotInclude { YES, NO } + + input NotUsed { + field: String + } + + input ShowFilter { + title: String + showType: ShowType + similarTo: SimilarityInput + } + + input SimilarityInput { + tags: [String] + } + + enum ShowType { + MOVIE, SERIES + } + + input CountryFilter { + countriesToExclude: [String] + } + + enum SourceType { FOO, BAR } + + type Person { + name: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + includeMutations = setOf("shows"), + generateDataTypes = false, + writeToFiles = false + ) + ).generate() + + assertThat(codeGenResult.javaDataTypes) + .extracting("typeSpec").extracting("name").containsExactly("ShowFilter", "SimilarityInput", "CountryFilter") + assertThat(codeGenResult.javaEnumTypes) + .extracting("typeSpec").extracting("name").containsExactly("ShowType", "SourceType") + assertThat(codeGenResult.javaQueryTypes) + .extracting("typeSpec").extracting("name").containsExactly("ShowsGraphQLQuery") + assertThat(codeGenResult.clientProjections) + .extracting("typeSpec").extracting("name").containsExactly("ShowsProjectionRoot", "BooleanProjection") + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaDataTypes + codeGenResult.javaEnumTypes) + } + + @Test + fun includeMutationConfig() { + val schema = """ + type Mutation { + updateMovieTitle: String + addActorName: Boolean + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + includeMutations = setOf("updateMovieTitle") + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("UpdateMovieTitleGraphQLQuery") + + assertCompilesJava(codeGenResult) + } +} diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenProjectionTestv2.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenProjectionTestv2.kt new file mode 100644 index 000000000..761264101 --- /dev/null +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenProjectionTestv2.kt @@ -0,0 +1,765 @@ +/* + * + * 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.clientapiv2 + +import com.netflix.graphql.dgs.codegen.CodeGen +import com.netflix.graphql.dgs.codegen.CodeGenConfig +import com.netflix.graphql.dgs.codegen.assertCompilesJava +import com.netflix.graphql.dgs.codegen.basePackageName +import com.squareup.javapoet.TypeVariableName +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.fail + +class ClientApiGenProjectionTestv2 { + @Test + fun generateProjectionRoot() { + val schema = """ + type Query { + people: [Person] + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(1) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("PeopleProjectionRoot") + + assertCompilesJava(codeGenResult.clientProjections) + } + + @Test + fun generateProjectionRootTestWithCycles() { + val schema = """ + type Query @extends { + persons: [Person] + } + + type Person { + name: String + friends: [Person] + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + assertThat(codeGenResult.clientProjections.size).isEqualTo(2) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("PersonsProjectionRoot") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("PersonProjection") + assertThat(codeGenResult.clientProjections[0].typeSpec.methodSpecs).extracting("name").contains("name") + assertThat(codeGenResult.clientProjections[0].typeSpec.methodSpecs).extracting("name").contains("friends") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name").contains("name") + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes) + } + + @Test + fun generateInterfaceProjectionsWithCycles() { + val schema = """ + type Query { + search(title: String): [Show] + } + + interface Show { + title: String + } + + type Movie implements Show { + title: String + duration: Int + details: Details + } + + type Details { + show: Show + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(4) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("SearchProjectionRoot") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("MovieProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec.name).isEqualTo("DetailsProjection") + assertThat(codeGenResult.clientProjections[3].typeSpec.name).isEqualTo("ShowProjection") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + codeGenResult.javaDataTypes + codeGenResult.javaInterfaces + ) + } + + @Test + fun generateUnionProjectionsWithCycles() { + val schema = """ + type Query { + search(title: String): [Video] + } + + union Video = Show | Movie + + type Show { + title: String + } + + type Movie { + title: String + duration: Int + related: Related + } + + type Related { + video: Video + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(5) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("SearchProjectionRoot") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("ShowProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec.name).isEqualTo("MovieProjection") + assertThat(codeGenResult.clientProjections[3].typeSpec.name).isEqualTo("RelatedProjection") + assertThat(codeGenResult.clientProjections[4].typeSpec.name).isEqualTo("VideoProjection") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + codeGenResult.javaDataTypes + codeGenResult.javaInterfaces + ) + } + + @Test + fun generateSubProjectionsWithDifferentRootTypes() { + val schema = """ + type Query @extends { + persons: [Person] + friends: [Person] + } + + type Person { + name: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("PersonsProjectionRoot") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("FriendsProjectionRoot") + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes) + } + + @Test + fun generateSubProjectionsWithDifferentParentTypes() { + val schema = """ + type Query @extends { + persons: [Person] + details(name: String): Details + } + + type Person { + details: Details + } + + type Details { + name: String + age: Integer + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("PersonsProjectionRoot") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("DetailsProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec.name).isEqualTo("DetailsProjectionRoot") + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes) + } + + @Test + fun generateSubProjectionTypes() { + val schema = """ + type Query { + movies: [Movie] + } + + type Movie { + title: String + actors: [Actor] + type: MovieType + } + + type Actor { + name: String + age: Integer + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(2) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("MoviesProjectionRoot") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("ActorProjection") + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes) + } + + @Test + fun generateSubProjectionTypesWithSimilarQueryAndFieldNames() { + val schema = """ + type Query { + user: User + } + + type User { + favoriteMovie: Movie + favoriteMovieGenre: Genre + } + + type Movie { + genre: Genre + } + + type Genre { + name: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(3) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("UserProjectionRoot") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("MovieProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec.name).isEqualTo("GenreProjection") + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes) + } + + @Test + fun generateSubProjectionTypesWithShortNames() { + val schema = """ + type Query { + movies: [Movie] + } + + type Movie { + title: String + actors: [Actor] + } + + type Actor { + name: String + age: Integer + movies: [Movie] + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + shortProjectionNames = true + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(3) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("MoviesProjectionRoot") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("ActorProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec.name).isEqualTo("MovieProjection") + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes) + } + + @Test + fun testExtendRootProjection() { + val schema = """ + type Query { + people: [Person] + } + + type Person { + name: String + } + + extend type Person { + email: String + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + typeMapping = mapOf("Long" to "java.lang.Long") + ) + ).generate() + val projections = codeGenResult.clientProjections + assertThat(projections.size).isEqualTo(1) + assertThat(projections[0].typeSpec.name).isEqualTo("PeopleProjectionRoot") + assertThat(projections[0].typeSpec.methodSpecs.size).isEqualTo(3) + assertThat(projections[0].typeSpec.methodSpecs).extracting("name").contains("name", "email") + + assertCompilesJava(codeGenResult) + } + + @Test + fun testExtendSubProjection() { + val schema = """ + type Query { + search: [SearchResult] + } + + type SearchResult { + movie: Movie + } + + type Movie { + title: String + } + + extend type Movie { + director: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + typeMapping = mapOf("Long" to "java.lang.Long") + ) + ).generate() + val projections = codeGenResult.clientProjections + assertThat(projections.size).isEqualTo(2) + assertThat(projections[1].typeSpec.name).isEqualTo("MovieProjection") + assertThat(projections[1].typeSpec.methodSpecs.size).isEqualTo(3) + assertThat(projections[1].typeSpec.methodSpecs).extracting("name").contains("title", "director", "") + + assertCompilesJava(codeGenResult) + } + + @Test + fun testExtendSubProjectionOutOfOrder() { + val schema = """ + type Query { + search: [SearchResult] + } + + type SearchResult { + movie: Movie + } + + extend type Movie { + director: String + } + + type Movie { + title: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + typeMapping = mapOf("Long" to "java.lang.Long") + ) + ).generate() + val projections = codeGenResult.clientProjections + assertThat(projections.size).isEqualTo(2) + assertThat(projections[1].typeSpec.name).isEqualTo("MovieProjection") + assertThat(projections[1].typeSpec.methodSpecs.size).isEqualTo(3) + assertThat(projections[1].typeSpec.methodSpecs).extracting("name").contains("title", "director", "") + + assertCompilesJava(codeGenResult) + } + + @Test + fun generateSubProjectionTypesMaxDepth() { + val schema = """ + type Query { + movies: [Movie] + } + + type Movie { + title: String + rating: Rating + actors: [Actor] + } + + type Actor { + name: String + age: Integer + agent: Agent + } + + type Agent { + name: String + address : Address + } + + type Address { + street: String + } + + type Rating { + starts: Integer + review: Review + } + + type Review { + description: String + } + + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + maxProjectionDepth = 2 + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(5) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("MoviesProjectionRoot") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("RatingProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec.name).isEqualTo("ReviewProjection") + assertThat(codeGenResult.clientProjections[3].typeSpec.name).isEqualTo("ActorProjection") + assertThat(codeGenResult.clientProjections[4].typeSpec.name).isEqualTo("AgentProjection") + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes) + } + + @Test + fun testImplementsInterfaceProjection() { + val schema = """ + type Query { + search(title: String): [Show] + } + + interface Show { + title: String + director: Director + } + + interface Person { + name: String + } + + type Director implements Person { + name: String + shows: [Show] + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("SearchGraphQLQuery") + assertThat(codeGenResult.clientProjections.size).isEqualTo(3) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("SearchProjectionRoot") + assertThat(codeGenResult.clientProjections[0].typeSpec.methodSpecs).extracting("name").contains("director") + assertThat(codeGenResult.clientProjections[0].typeSpec.methodSpecs).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("DirectorProjection") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name").contains("shows") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs).extracting("name").contains("name") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + codeGenResult.javaDataTypes + codeGenResult.javaInterfaces + ) + } + + @Test + fun testScalarsDontGenerateProjections() { + val schema = """ + type Query { + movieCountry: MovieCountry + } + + type MovieCountry { + country: String + movieId: Long + } + scalar Long + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + typeMapping = mapOf("Long" to "java.lang.Long") + ) + ).generate() + val projections = codeGenResult.clientProjections + assertThat(projections.size).isEqualTo(1) + assertCompilesJava(codeGenResult) + } + + @Test + fun generateProjectionRootWithReservedNames() { + val schema = """ + type Query { + weirdType: WeirdType + } + + type WeirdType { + _: String + root: String + parent: String + import: String + short: Integer + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(1) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("WeirdTypeProjectionRoot") + assertThat(codeGenResult.clientProjections[0].typeSpec.methodSpecs).extracting("name") + .contains("__", "_root", "_parent", "_import", "_short") + + assertCompilesJava(codeGenResult) + } + + @Test + fun generateSubProjectionWithReservedNames() { + val schema = """ + type Query { + normalType: NormalType + } + + type NormalType { + weirdType: WeirdType + } + + type WeirdType { + _: String + root: String + parent: String + import: String + short: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(2) + val weirdType = codeGenResult.clientProjections.find { it.typeSpec.name == "WeirdTypeProjection" } + ?: fail("NormalType_WeirdTypeProjection type not found") + + assertThat(weirdType.typeSpec.methodSpecs).extracting("name") + .contains("__", "_root", "_parent", "_import", "_short") + + assertCompilesJava(codeGenResult.clientProjections) + } + + @Test + fun generateProjectionsForSameTypeInSameQueryWithDifferentPaths() { + val schema = """ + type Query { + workshop: Workshop + } + + type Workshop { + reviews: ReviewConnection + assets: Asset + } + + type ReviewConnection { + edges: [ReviewEdge] + } + + type ReviewEdge { + node: String + } + + type Asset { + reviews: ReviewConnection + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + writeToFiles = false + ) + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(4) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("WorkshopProjectionRoot") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("ReviewConnectionProjection") + assertThat(codeGenResult.clientProjections[3].typeSpec.name).isEqualTo("AssetProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec.name).isEqualTo("ReviewEdgeProjection") + } + + @Test + fun `Input arguments on root projections should be support in the query API`() { + val schema = """ + type Query { + movies: [Movie] + } + + type Movie { + actors(leadCharactersOnly: Boolean): [Actor] + } + + type Actor { + name: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + maxProjectionDepth = 2 + ) + ).generate() + + val methodSpecs = codeGenResult.clientProjections[0].typeSpec.methodSpecs + assertThat(methodSpecs.size).isEqualTo(3) + val methodWithArgs = methodSpecs.find { it.parameters.size > 0 && it.name == "actors" } + ?: fail("Expected method not found") + assertThat(methodWithArgs.parameters[0].name).isEqualTo("leadCharactersOnly") + assertThat(methodWithArgs.parameters[0].type.toString()).isEqualTo("java.lang.Boolean") + } + + @Test + fun `Input arguments on sub projections should be support in the query API`() { + val schema = """ + type Query { + movies: [Movie] + } + + type Movie { + actors: [Actor] + awards(oscarsOnly: Boolean): [Award!] + } + + type Actor { + awards(oscarsOnly: Boolean): [Award!] + } + + type Award { + name: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + writeToFiles = true + ) + ).generate() + + val methodSpecs = codeGenResult.clientProjections[1].typeSpec.methodSpecs + val methodWithArgs = methodSpecs.find { !it.isConstructor && it.parameters.size > 0 } + ?: fail("Method not found") + assertThat(methodWithArgs.returnType).extracting { (it as TypeVariableName).name } + .isEqualTo("AwardProjection, ROOT>") + assertThat(methodWithArgs.parameters[0].name).isEqualTo("oscarsOnly") + assertThat(methodWithArgs.parameters[0].type.toString()).isEqualTo("java.lang.Boolean") + } +} diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenQueryTestv2.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenQueryTestv2.kt new file mode 100644 index 000000000..32ced2daf --- /dev/null +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenQueryTestv2.kt @@ -0,0 +1,977 @@ +/* + * + * 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.clientapiv2 + +import com.netflix.graphql.dgs.codegen.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.Ignore +import org.junit.jupiter.api.Test + +@Ignore +class ClientApiGenQueryTestv2 { + @Test + fun generateQueryType() { + val schema = """ + type Query { + people: [Person] + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("PeopleGraphQLQuery") + codeGenResult.javaQueryTypes[0].typeSpec.methodSpecs.find { it -> it.isConstructor && it.parameters.isEmpty() } + codeGenResult.javaQueryTypes[0].typeSpec.methodSpecs.find { it -> it.isConstructor && (it.parameters.find { param -> param.name == "queryName" } != null) } + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes) + } + + @Test + fun generateQueryTypeWithComments() { + val schema = """ + type Query { + ""${'"'} + All the people + ""${'"'} + people: [Person] + } + + type Person { + firstname: String + lastname: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("PeopleGraphQLQuery") + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.javadoc.toString()).isEqualTo( + """ + All the people + """.trimIndent() + ) + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes) + } + + @Test + fun generateQueryTypesWithTypeExtensions() { + val schema = """ + extend type Person { + preferences: Preferences + } + + type Preferences { + userId: ID! + } + + type Query @extends { + getPerson: Person + } + + type Person { + personId: ID! + linkedIdentities: LinkedIdentities + } + + type LinkedIdentities { + employee: Employee + } + + type Employee { + id: ID! + person: Person! + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("GetPersonGraphQLQuery") + + assertCompilesJava(codeGenResult) + } + + @Test + fun generateOnlyRequiredDataTypesForQuery() { + val schema = """ + type Query { + shows(showFilter: ShowFilter): [Video] + people(personFilter: PersonFilter): [Person] + } + + union Video = Show | Movie + + type Movie { + title: String + duration: Int + related: Related + } + + type Related { + video: Video + } + + type Show { + title: String + tags(from: Int, to: Int, sourceType: SourceType): [ShowTag] + isLive(countryFilter: CountryFilter): Boolean + } + + enum ShouldNotInclude { YES, NO } + + input NotUsed { + field: String + } + + input ShowFilter { + title: String + showType: ShowType + similarTo: SimilarityInput + } + + input SimilarityInput { + tags: [String] + } + + enum ShowType { + MOVIE, SERIES + } + + input CountryFilter { + countriesToExclude: [String] + } + + enum SourceType { FOO, BAR } + + type Person { + name: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + includeQueries = setOf("shows"), + generateDataTypes = false, + writeToFiles = false + ) + ).generate() + + assertThat(codeGenResult.javaDataTypes) + .extracting("typeSpec").extracting("name").containsExactly("ShowFilter", "SimilarityInput", "CountryFilter") + assertThat(codeGenResult.javaEnumTypes) + .extracting("typeSpec").extracting("name").containsExactly("ShowType", "SourceType") + assertThat(codeGenResult.javaQueryTypes) + .extracting("typeSpec").extracting("name").containsExactly("ShowsGraphQLQuery") + assertThat(codeGenResult.clientProjections) + .extracting("typeSpec").extracting("name").containsExactly( + "ShowsProjectionRoot", + "ShowProjection", + "MovieProjection", + "RelatedProjection", + "VideoProjection" + ) + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaDataTypes + codeGenResult.javaEnumTypes) + } + + @Test + fun generateRecursiveInputTypes() { + val schema = """ + type Query { + movies(filter: MovieQuery): [String] + } + + input MovieQuery { + booleanQuery: BooleanQuery! + titleFilter: String + } + + input BooleanQuery { + first: MovieQuery! + second: MovieQuery! + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateDataTypes = false, + generateClientApiv2 = true, + includeQueries = setOf("movies") + ) + ).generate() + + assertThat(codeGenResult.javaDataTypes.size).isEqualTo(2) + assertThat(codeGenResult.javaDataTypes[0].typeSpec.name).isEqualTo("MovieQuery") + assertThat(codeGenResult.javaDataTypes[1].typeSpec.name).isEqualTo("BooleanQuery") + + assertCompilesJava(codeGenResult.javaDataTypes) + } + + @Test + fun generateArgumentsForSimpleTypes() { + val schema = """ + type Query { + personSearch(lastname: String): [Person] + } + + type Person { + firstname: String + lastname: String + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.typeSpecs[0].methodSpecs[1].name).isEqualTo("lastname") + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes) + } + + @Test + fun generateArgumentsForEnum() { + val schema = """ + type Query { + personSearch(index: SearchIndex): [Person] + } + + type Person { + firstname: String + lastname: String + } + + enum SearchIndex { + TEST, PROD + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.typeSpecs[0].methodSpecs[1].name).isEqualTo("index") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + ) + } + + @Test + fun generateArgumentsForObjectType() { + val schema = """ + type Query { + personSearch(index: SearchIndex): [Person] + } + + type Person { + firstname: String + lastname: String + } + + type SearchIndex { + name: String + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("PersonSearchGraphQLQuery") + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.typeSpecs[0].methodSpecs[1].name).isEqualTo("index") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + codeGenResult.javaDataTypes + ) + } + + @Test + fun includeQueryConfig() { + val schema = """ + type Query { + movieTitles: [String] + actorNames: [String] + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + includeQueries = setOf("movieTitles") + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("MovieTitlesGraphQLQuery") + + assertCompilesJava(codeGenResult) + } + + @Test + fun skipCodegen() { + val schema = """ + type Query { + persons: [Person] + personSearch(index: SearchIndex): [Person] @skipcodegen + } + + type Person { + firstname: String + lastname: String + } + + type SearchIndex { + name: String + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("PersonsGraphQLQuery") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + codeGenResult.javaDataTypes + ) + } + + @Test + fun interfaceReturnTypes() { + val schema = """ + type Query { + search(title: String): [Show] + } + + interface Show { + title: String + } + + type Movie implements Show { + title: String + duration: Int + } + + type Series implements Show { + title: String + episodes: Int + } + + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("SearchGraphQLQuery") + assertThat(codeGenResult.clientProjections.size).isEqualTo(3) + assertThat(codeGenResult.clientProjections[0].typeSpec.name).isEqualTo("SearchProjectionRoot") + assertThat(codeGenResult.clientProjections[0].typeSpec.methodSpecs[1].name).isEqualTo("title") + assertThat(codeGenResult.clientProjections[1].typeSpec.name).isEqualTo("MovieProjection") + assertThat(codeGenResult.clientProjections[1].typeSpec.methodSpecs[2].name).isEqualTo("duration") + assertThat(codeGenResult.clientProjections[2].typeSpec.name).isEqualTo("SeriesProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec.methodSpecs[2].name).isEqualTo("episodes") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + codeGenResult.javaDataTypes + codeGenResult.javaInterfaces + ) + } + + @Test + fun interfaceWithKeywords() { + val schema = """ + type Query { + queryRoot: QueryRoot + } + + interface HasDefaultField { + default: String + public: String + private: Boolean + } + + type QueryRoot implements HasDefaultField { + name: String + default: String + public: String + private: Boolean + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("QueryRootGraphQLQuery") + + assertThat(codeGenResult.javaInterfaces.size).isEqualTo(1) + assertThat(codeGenResult.javaInterfaces[0].typeSpec.name).isEqualTo("HasDefaultField") + + assertThat(codeGenResult.javaDataTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaDataTypes[0].typeSpec.fieldSpecs.size).isEqualTo(4) + assertThat(codeGenResult.javaDataTypes[0].typeSpec.fieldSpecs[0].name).isEqualTo("name") + assertThat(codeGenResult.javaDataTypes[0].typeSpec.fieldSpecs[1].name).isEqualTo("_default") + assertThat(codeGenResult.javaDataTypes[0].typeSpec.fieldSpecs[2].name).isEqualTo("_public") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + codeGenResult.javaDataTypes + codeGenResult.javaInterfaces + ) + } + + @Test + fun `The Query API should support sub-projects on fields with Basic Types`() { + // given + val schema = """ + type Query { + someField: Foo + } + + type Foo { + stringField(arg: Boolean): String + stringArrayField(arg: Boolean): [String] + intField(arg: Boolean): Int + intArrayField(arg: Boolean): [Int] + booleanField(arg: Boolean): Boolean + booleanArrayField(arg: Boolean): [Boolean] + floatField(arg: Boolean): Float + floatArrayField(arg: Boolean): [Float] + } + """.trimIndent() + // when + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + writeToFiles = true + ) + ).generate() + // then + val testClassLoader = assertCompilesJava(codeGenResult).toClassLoader() + // assert Type classes + assertThat(testClassLoader.loadClass("$basePackageName.types.Foo")).isNotNull + // assert root projection classes + val rootProjectionClass = + testClassLoader.loadClass("$basePackageName.client.SomeFieldProjectionRoot") + assertThat(rootProjectionClass).isNotNull + assertThat(rootProjectionClass).hasPublicMethods( + "stringField", + "stringArrayField", + "intField", + "intArrayField", + "booleanField", + "booleanArrayField", + "floatField", + "floatArrayField" + ) + // fields projections + assertThat(rootProjectionClass).isNotNull + // stringField + assertThat( + rootProjectionClass.getMethod("stringField") + ).isNotNull + .returns(rootProjectionClass) { it.returnType } + + assertThat( + rootProjectionClass.getMethod( + "stringField", + java.lang.Boolean::class.java + ) + ).isNotNull + .extracting { m -> m.parameters.mapIndexed { index, parameter -> index to parameter.name } } + .asList() + .containsExactly(0 to "arg") + // stringArrayField + assertThat( + rootProjectionClass.getMethod("stringArrayField") + ).isNotNull + .returns(rootProjectionClass) { it.returnType } + + assertThat( + rootProjectionClass.getMethod( + "stringArrayField", + java.lang.Boolean::class.java + ) + ).isNotNull + .extracting { m -> m.parameters.mapIndexed { index, parameter -> index to parameter.name } } + .asList() + .containsExactly(0 to "arg") + + // booleanField + assertThat( + rootProjectionClass.getMethod("booleanField") + ).isNotNull + .returns(rootProjectionClass) { it.returnType } + + assertThat( + rootProjectionClass.getMethod( + "booleanField", + java.lang.Boolean::class.java + ) + ).isNotNull + .extracting { m -> m.parameters.mapIndexed { index, parameter -> index to parameter.name } } + .asList() + .containsExactly(0 to "arg") + + // booleanArrayField + assertThat( + rootProjectionClass.getMethod("booleanArrayField") + ).isNotNull + .returns(rootProjectionClass) { it.returnType } + + assertThat( + rootProjectionClass.getMethod( + "booleanArrayField", + java.lang.Boolean::class.java + ) + ).isNotNull + .extracting { m -> m.parameters.mapIndexed { index, parameter -> index to parameter.name } } + .asList() + .containsExactly(0 to "arg") + + // floatField + assertThat( + rootProjectionClass.getMethod("floatField") + ).isNotNull + .returns(rootProjectionClass) { it.returnType } + + assertThat( + rootProjectionClass.getMethod( + "floatField", + java.lang.Boolean::class.java + ) + ).isNotNull + .extracting { m -> m.parameters.mapIndexed { index, parameter -> index to parameter.name } } + .asList() + .containsExactly(0 to "arg") + + // booleanArrayField + assertThat( + rootProjectionClass.getMethod("floatArrayField") + ).isNotNull + .returns(rootProjectionClass) { it.returnType } + + assertThat( + rootProjectionClass.getMethod( + "floatArrayField", + java.lang.Boolean::class.java + ) + ).isNotNull + .extracting { m -> m.parameters.mapIndexed { index, parameter -> index to parameter.name } } + .asList() + .containsExactly(0 to "arg") + } + + @Test + fun `The Query API should support sub-projects on fields with Scalars`() { + val schema = """ + type Query { + someField: Foo + } + + type Foo { + ping(arg: Boolean): Long + } + + scalar Long + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + typeMapping = mapOf("Long" to "java.lang.Long") + ) + ).generate() + val projections = codeGenResult.clientProjections + assertThat(projections.size).isEqualTo(2) + + val testClassLoader = assertCompilesJava(codeGenResult).toClassLoader() + // assert Type classes + assertThat(testClassLoader.loadClass("$basePackageName.types.Foo")).isNotNull + // assert root projection classes + val rootProjectionClass = + testClassLoader.loadClass("$basePackageName.client.SomeFieldProjectionRoot") + assertThat(rootProjectionClass).isNotNull + assertThat(rootProjectionClass).hasPublicMethods("ping") + // scalar field + assertThat(rootProjectionClass.getMethod("ping")).isNotNull.returns(rootProjectionClass) { it.returnType } + + assertThat( + rootProjectionClass.getMethod("ping", java.lang.Boolean::class.java) + ).isNotNull + .extracting { m -> m.parameters.mapIndexed { index, parameter -> index to parameter.name } } + .asList() + .containsExactly(0 to "arg") + } + + @Test + fun `Should be able to generate a valid client when java keywords are used as field names`() { + val schema = """ + type Query { + someField: Foo + } + + type Foo { + ping(arg: Boolean): Long + # --- + parent: Boolean + root: Boolean + # --- + abstract: Boolean + assert: Boolean + boolean: Boolean + break: Boolean + byte: Boolean + case: Boolean + catch: Boolean + char: Boolean + # class: Boolean -- not supported + const: Boolean + continue: Boolean + default: Boolean + do: Boolean + double: Boolean + else: Boolean + enum: Boolean + extends: Boolean + final: Boolean + finally: Boolean + float: Boolean + for: Boolean + goto: Boolean + if: Boolean + implements: Boolean + import: Boolean + instanceof: Boolean + int: Boolean + interface: Boolean + long: Boolean + native: Boolean + new: Boolean + package: Boolean + private: Boolean + protected: Boolean + public: Boolean + return: Boolean + short: Boolean + static: Boolean + strictfp: Boolean + super: Boolean + switch: Boolean + synchronized: Boolean + this: Boolean + throw: Boolean + throws: Boolean + transient: Boolean + try: Boolean + void: Boolean + volatile: Boolean + while: Boolean + class: Int + } + + scalar Long + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateDataTypes = true, + generateClientApiv2 = true, + typeMapping = mapOf("Long" to "java.lang.Long") + ) + ).generate() + val projections = codeGenResult.clientProjections + assertThat(projections.size).isEqualTo(2) + + val testClassLoader = assertCompilesJava(codeGenResult).toClassLoader() + // assert Type classes + assertThat(testClassLoader.loadClass("$basePackageName.types.Foo")).isNotNull + // assert root projection classes + val rootProjectionClass = + testClassLoader.loadClass("$basePackageName.client.SomeFieldProjectionRoot") + assertThat(rootProjectionClass).isNotNull + assertThat(rootProjectionClass).hasPublicMethods("ping") + assertThat(rootProjectionClass).hasPublicMethods( + "_parent", + "_root", + // ---- + "_abstract", + "_assert", + "_boolean", + "_break", + "_byte", + "_case", + "_catch", + "_char", + "_const", + "_continue", + "_default", + "_do", + "_double", + "_else", + "_enum", + "_extends", + "_final", + "_finally", + "_float", + "_for", + "_goto", + "_if", + "_implements", + "_import", + "_instanceof", + "_int", + "_interface", + "_long", + "_native", + "_new", + "_package", + "_private", + "_protected", + "_public", + "_return", + "_short", + "_static", + "_strictfp", + "_super", + "_switch", + "_synchronized", + "_this", + "_throw", + "_throws", + "_transient", + "_try", + "_void", + "_volatile", + "_while", + "_class" + ) + } + + @Test + fun `Should be able to generate successfully when java keywords and default value are used as input types`() { + val schema = """ + type Query { + foo(fooInput: FooInput): Baz + bar(barInput: BarInput): Baz + } + + input FooInput { + public: Boolean = true + } + + input BarInput { + public: Boolean + } + + type Baz { + public: Boolean + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateDataTypes = false, + generateClientApiv2 = true, + includeQueries = setOf("foo", "bar") + ) + ).generate() + + assertThat(codeGenResult.javaDataTypes.size).isEqualTo(2) + + assertThat(codeGenResult.javaDataTypes[0].typeSpec.name).isEqualTo("FooInput") + assertThat(codeGenResult.javaDataTypes[0].typeSpec.fieldSpecs[0].name).isEqualTo("_public") + assertThat(codeGenResult.javaDataTypes[0].typeSpec.fieldSpecs[0].initializer.toString()).isEqualTo("true") + + assertThat(codeGenResult.javaDataTypes[1].typeSpec.name).isEqualTo("BarInput") + assertThat(codeGenResult.javaDataTypes[1].typeSpec.fieldSpecs[0].initializer.toString()).isEqualTo("") + + assertCompilesJava(codeGenResult.javaDataTypes) + } + + @Test + fun `generate client code for both query and subscription with same definitions`() { + val schema = """ + type Subscription { + shows: [Show] + movie(id: ID!): Movie + foo: Boolean + bar: Boolean + } + + type Mutation { + shows: [String] + movie(id: ID!, title: String): Movie + foo: String + } + + type Query { + shows: [Show] + movie: Movie + } + type Show { + id: Int + title: String + } + + type Movie { + title: String + duration: Int + related: Related + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(9) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("ShowsGraphQLQuery") + assertThat(codeGenResult.javaQueryTypes[1].typeSpec.name).isEqualTo("MovieGraphQLQuery") + + assertThat(codeGenResult.javaQueryTypes[2].typeSpec.name).isEqualTo("ShowsGraphQLMutation") + assertThat(codeGenResult.javaQueryTypes[3].typeSpec.name).isEqualTo("MovieGraphQLMutation") + assertThat(codeGenResult.javaQueryTypes[4].typeSpec.name).isEqualTo("FooGraphQLQuery") + + assertThat(codeGenResult.javaQueryTypes[5].typeSpec.name).isEqualTo("ShowsGraphQLSubscription") + assertThat(codeGenResult.javaQueryTypes[6].typeSpec.name).isEqualTo("MovieGraphQLSubscription") + assertThat(codeGenResult.javaQueryTypes[7].typeSpec.name).isEqualTo("FooGraphQLSubscription") + assertThat(codeGenResult.javaQueryTypes[8].typeSpec.name).isEqualTo("BarGraphQLQuery") + + assertCompilesJava(codeGenResult.javaQueryTypes) + } + + @Test + fun `Should be able to generate successfully when java keywords are used as types`() { + val schema = """ + type Query { + bar: Bar + } + + interface Foo { + class: Int + } + + type Bar implements Foo { + object: Int + class: Int + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateDataTypes = true, + generateClientApiv2 = true, + includeQueries = setOf("bar") + ) + ).generate() + + assertThat(codeGenResult.javaDataTypes.size).isEqualTo(1) + + val typeSpec = codeGenResult.javaDataTypes[0].typeSpec + assertThat(typeSpec.name).isEqualTo("Bar") + assertThat(typeSpec.fieldSpecs[0].name).isEqualTo("object") + assertThat(typeSpec.fieldSpecs.size).isEqualTo(2) + assertThat(typeSpec.fieldSpecs[1].name).isEqualTo("_class") + + assertThat(typeSpec.methodSpecs.size).isGreaterThan(0) + assertThat(typeSpec.methodSpecs[0].name).isEqualTo("getObject") + assertThat(typeSpec.methodSpecs[1].name).isEqualTo("setObject") + assertThat(typeSpec.methodSpecs[2].name).isEqualTo("getClassField") + assertThat(typeSpec.methodSpecs[3].name).isEqualTo("setClassField") + + assertCompilesJava(codeGenResult.javaDataTypes + codeGenResult.javaInterfaces) + } +} diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenSubscriptionTestv2.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenSubscriptionTestv2.kt new file mode 100644 index 000000000..ad46f2624 --- /dev/null +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapiv2/ClientApiGenSubscriptionTestv2.kt @@ -0,0 +1,114 @@ +/* + * + * 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.clientapiv2 + +import com.netflix.graphql.dgs.codegen.CodeGen +import com.netflix.graphql.dgs.codegen.CodeGenConfig +import com.netflix.graphql.dgs.codegen.assertCompilesJava +import com.netflix.graphql.dgs.codegen.basePackageName +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class ClientApiGenSubscriptionTestv2 { + @Test + fun generateSubscriptionType() { + val schema = """ + type Subscription { + movie(movieId: ID, title: String): Movie + } + + type Movie { + movieId: ID + title: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("MovieGraphQLQuery") + + assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes) + } + + @Test + fun generateSubscriptionWithInputType() { + val schema = """ + type Mutation { + movie(movie: MovieDescription): Movie + } + + input MovieDescription { + movieId: ID + title: String + actors: [String] + } + + type Movie { + movieId: ID + lastname: String + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("MovieGraphQLQuery") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaDataTypes + ) + } + + @Test + fun includeSubscriptionConfig() { + val schema = """ + type Subscription { + movieTitle: String + addActorName: Boolean + } + """.trimIndent() + + val codeGenResult = CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = basePackageName, + generateClientApiv2 = true, + includeSubscriptions = setOf("movieTitle") + ) + ).generate() + + assertThat(codeGenResult.javaQueryTypes.size).isEqualTo(1) + assertThat(codeGenResult.javaQueryTypes[0].typeSpec.name).isEqualTo("MovieTitleGraphQLQuery") + + assertCompilesJava(codeGenResult) + } +} 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 505675d71..ff10e87b6 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 @@ -66,6 +66,9 @@ open class GenerateJavaTask : DefaultTask() { @Input var generateClient = false + @Input + var generateClientApiv2 = false + @Input var generateKotlinNullableClasses = false @@ -178,6 +181,7 @@ open class GenerateJavaTask : DefaultTask() { language = Language.valueOf(language.uppercase(Locale.getDefault())), generateBoxedTypes = generateBoxedTypes, generateClientApi = generateClient, + generateClientApiv2 = generateClientApiv2, generateKotlinNullableClasses = generateKotlinNullableClasses, generateKotlinClosureProjections = generateKotlinClosureProjections, generateInterfaces = generateInterfaces, diff --git a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginEntitySmokeTest.kt b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginEntitySmokeTest.kt index 5f45d756a..18b02a0ae 100644 --- a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginEntitySmokeTest.kt +++ b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginEntitySmokeTest.kt @@ -18,10 +18,6 @@ package com.netflix.graphql.dgs -import org.assertj.core.api.Assertions.assertThat -import org.gradle.testkit.runner.GradleRunner -import org.gradle.testkit.runner.TaskOutcome.SUCCESS -import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.io.File @@ -30,81 +26,6 @@ class CodegenGradlePluginEntitySmokeTest { @TempDir lateinit var projectDir: File - @Test - fun `Nested input and enum types are generated even if data type generation is disabled explicitly`() { - prepareBuildGraphQLSchema( - """ - directive @key(fields: String) on OBJECT - - type Query { - movies(from: Int, to: Int, movieIds: [Int]): [Movie] - } - - type Movie @key(fields : "movieId") { - movieId: Int! - title: String - tags(from: Int, to: Int, sourceType: SourceType): [MovieTag] - isLive(countryFilter: CountryFilter): Boolean - } - - input CountryFilter { - countriesToExclude: [String] - } - - type MovieTag { - movieId: Long - tagId: Long - sourceType: SourceType - tagValues(from: Int, to: Int): [String] - } - - enum SourceType { - FOO - BAR - } - """.trimMargin() - ) - - prepareBuildGradleFile( - """ - plugins { - id 'java' - id 'com.netflix.dgs.codegen' - } - - repositories { - mavenCentral() - } - - generateJava { - packageName = 'com.netflix.testproject.graphql' - generateClient = true - generateDataTypes = false - includeQueries = ["movies"] - typeMapping = [ - Long: "java.lang.Long", - ] - } - // Need to disable the core conventions since the artifacts are not yet visible. - codegen.clientCoreConventionsEnabled = false - """.trimMargin() - ) - - val result = GradleRunner.create() - .withProjectDir(projectDir) - .withPluginClasspath() - .withDebug(true) - .withArguments( - "--stacktrace", - "--info", - "generateJava", - "build" - ).build() - - assertThat(result.task(":generateJava")).extracting { it?.outcome }.isEqualTo(SUCCESS) - assertThat(result.task(":build")).extracting { it?.outcome }.isEqualTo(SUCCESS) - } - private fun prepareBuildGradleFile(content: String) { writeProjectFile("build.gradle", content) } diff --git a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginSpringBootSmokeTest.kt b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginSpringBootSmokeTest.kt index 6c28daa01..438e2398d 100644 --- a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginSpringBootSmokeTest.kt +++ b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginSpringBootSmokeTest.kt @@ -18,10 +18,6 @@ package com.netflix.graphql.dgs -import org.assertj.core.api.Assertions.assertThat -import org.gradle.testkit.runner.GradleRunner -import org.gradle.testkit.runner.TaskOutcome.SUCCESS -import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.io.File @@ -79,204 +75,6 @@ class CodegenGradlePluginSpringBootSmokeTest { scalar Url """.trimMargin() - @Test - fun `A SpringBoot project can use the generated Java`() { - prepareBuildGraphQLSchema(graphQLSchema) - - prepareBuildGradleFile( - """ - plugins { - id 'java' - id 'com.netflix.dgs.codegen' - } - - repositories { - mavenCentral() - } - - generateJava { - packageName = 'com.netflix.testproject.graphql' - typeMapping = [ - DateTime: "java.time.OffsetTime", - Time: "java.time.OffsetDateTime", - Date: "java.time.LocalDate", - JSON: "java.lang.Object", - Url: "java.net.URL" - ] - snakeCaseConstantNames = true - } - - dependencies { - implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:latest.release")) - implementation("com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter") - implementation("com.netflix.graphql.dgs:graphql-dgs-extended-scalars") - testImplementation("org.springframework.boot:spring-boot-starter-test") - } - - java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) - } - } - - test { - useJUnitPlatform() - } - // Need to disable the core conventions since the artifacts are not yet visible. - codegen.clientCoreConventionsEnabled = false - """.trimMargin() - ) - - writeProjectFile( - "src/test/java/AppTest.java", - """ - import org.springframework.context.annotation.Configuration; - import org.springframework.boot.autoconfigure.EnableAutoConfiguration; - import org.springframework.boot.test.context.SpringBootTest; - import org.junit.jupiter.api.Test; - - import com.netflix.testproject.graphql.types.Filter; - import com.netflix.testproject.graphql.types.Result; - import com.netflix.testproject.graphql.types.JSONMetaData; - import com.netflix.testproject.graphql.DgsConstants; - import com.netflix.testproject.graphql.DgsConstants.QUERY; - import com.netflix.testproject.graphql.DgsConstants.RESULT; - import com.netflix.testproject.graphql.DgsConstants.FILTER; - import com.netflix.testproject.graphql.DgsConstants.JSON_META_DATA; - import com.netflix.testproject.graphql.DgsConstants.SIMPLE_META_DATA; - - - @SpringBootTest( - classes = {AppTest.TestConf.class}, - properties = { "debug=true" } - ) - @EnableAutoConfiguration - public class AppTest { - - @Test - public void test(){ - } - - @Configuration - static class TestConf { } - } - """.trimIndent() - ) - - val result = GradleRunner.create() - .withProjectDir(projectDir) - .withPluginClasspath() - .withDebug(true) - .withArguments( - "--stacktrace", - "--info", - "generateJava", - "check" - ).build() - - assertThat(result.task(":generateJava")).extracting { it?.outcome }.isEqualTo(SUCCESS) - assertThat(result.task(":check")).extracting { it?.outcome }.isEqualTo(SUCCESS) - } - - @Test - fun `A Spring Boot project can generate the generated Kotlin classes and objects`() { - prepareBuildGradleFile( - """ - plugins { - id 'java' - id 'org.jetbrains.kotlin.jvm' version '1.6.21' - id 'com.netflix.dgs.codegen' - } - - repositories { - mavenCentral() - } - - generateJava { - packageName = 'com.netflix.testproject.graphql' - typeMapping = [ - DateTime: "java.time.OffsetTime", - Time: "java.time.OffsetDateTime", - Date: "java.time.LocalDate", - JSON: "java.lang.Object", - Url: "java.net.URL" - ] - language = "KOTLIN" - snakeCaseConstantNames = true - } - - dependencies { - implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:latest.release")) - implementation("com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter") - implementation("com.netflix.graphql.dgs:graphql-dgs-extended-scalars") - testImplementation("org.springframework.boot:spring-boot-starter-test") - } - - java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) - } - } - - test { - useJUnitPlatform() - } - // Need to disable the core conventions since the artifacts are not yet visible. - codegen.clientCoreConventionsEnabled = false - """.trimMargin() - ) - - writeProjectFile( - "src/test/kotlin/AppTest.kt", - """ - import org.springframework.context.annotation.Configuration - import org.springframework.boot.autoconfigure.EnableAutoConfiguration - import org.springframework.boot.test.context.SpringBootTest - import org.junit.jupiter.api.Test - - import com.netflix.testproject.graphql.types.Filter - import com.netflix.testproject.graphql.types.Result - import com.netflix.testproject.graphql.types.JSONMetaData - import com.netflix.testproject.graphql.DgsConstants - import com.netflix.testproject.graphql.DgsConstants.QUERY - import com.netflix.testproject.graphql.DgsConstants.RESULT - import com.netflix.testproject.graphql.DgsConstants.FILTER - import com.netflix.testproject.graphql.DgsConstants.JSON_META_DATA - import com.netflix.testproject.graphql.DgsConstants.SIMPLE_META_DATA - - - @SpringBootTest( - classes=[AppTest.TestConf::class], - properties=["debug=true"] - ) - @EnableAutoConfiguration - internal class AppTest{ - - @Test - fun test() {} - - @Configuration - open class TestConf { } - } - """.trimIndent() - ) - - prepareBuildGraphQLSchema(graphQLSchema) - - val result = GradleRunner.create() - .withProjectDir(projectDir) - .withPluginClasspath() - .withDebug(true) - .withArguments( - "--stacktrace", - "generateJava", - "check" - ).build() - - assertThat(result.task(":generateJava")).extracting { it?.outcome }.isEqualTo(SUCCESS) - assertThat(result.task(":check")).extracting { it?.outcome }.isEqualTo(SUCCESS) - } - private fun prepareBuildGradleFile(content: String) { writeProjectFile("build.gradle", content) } diff --git a/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/BaseProjectionNode.kt b/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/BaseProjectionNode.kt index 67edf28a3..b4a484b67 100644 --- a/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/BaseProjectionNode.kt +++ b/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/BaseProjectionNode.kt @@ -33,7 +33,7 @@ abstract class BaseProjectionNode( ) { val fields: MutableMap = LinkedHashMap() - val fragments: MutableList> = LinkedList() + val fragments: MutableList = LinkedList() val inputArguments: MutableMap> = LinkedHashMap() data class InputArgument(val name: String, val value: Any?) diff --git a/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequest.kt b/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequest.kt index 3aa742e4f..cf32f1d15 100644 --- a/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequest.kt +++ b/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequest.kt @@ -54,7 +54,7 @@ class GraphQLQueryRequest( } if (projection != null) { - val selectionSet = if (projection is BaseSubProjectionNode<*, *>) { + val selectionSet = if (projection is BaseSubProjectionNode<*, *> && projection.root() != null) { projectionSerializer.toSelectionSet(projection.root() as BaseProjectionNode) } else { projectionSerializer.toSelectionSet(projection)