diff --git a/plugins/client/graphql-kotlin-client-generator/README.md b/plugins/client/graphql-kotlin-client-generator/README.md index f32bd2d477..44906770b4 100755 --- a/plugins/client/graphql-kotlin-client-generator/README.md +++ b/plugins/client/graphql-kotlin-client-generator/README.md @@ -27,5 +27,5 @@ using [square/kotlinpoet](https://github.com/square/kotlinpoet) library. ## Code Generation Limitations -* Only a single operation per GraphQL query file is supported. +* Only a single operation per GraphQL query file is supported. To avoid duplication of return types, a shared fragments file can be used. * Subscriptions are currently NOT supported. diff --git a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generateClient.kt b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generateClient.kt index b134035fd8..a337b68c75 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generateClient.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generateClient.kt @@ -35,7 +35,8 @@ fun generateClient( schemaPath: String, queries: List, useOptionalInputWrapper: Boolean = false, - parserOptions: ParserOptions.Builder.() -> Unit = {} + parserOptions: ParserOptions.Builder.() -> Unit = {}, + fragmentsFile: File? ): List { val customScalars = customScalarsMap.associateBy { it.scalar } val config = GraphQLClientGeneratorConfig( @@ -47,5 +48,5 @@ fun generateClient( parserOptions = parserOptions ) val generator = GraphQLClientGenerator(schemaPath, config) - return generator.generate(queries) + return generator.generate(queries, fragmentsFile) } diff --git a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGenerator.kt b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGenerator.kt index e4b02c55ec..77e5202f3e 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGenerator.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGenerator.kt @@ -17,6 +17,7 @@ package com.expediagroup.graphql.plugin.client.generator import com.expediagroup.graphql.client.Generated +import com.expediagroup.graphql.plugin.client.generator.exceptions.InvalidFragmentFileException import com.expediagroup.graphql.plugin.client.generator.exceptions.MultipleOperationsInFileException import com.expediagroup.graphql.plugin.client.generator.exceptions.SchemaUnavailableException import com.expediagroup.graphql.plugin.client.generator.types.generateGraphQLObjectTypeSpec @@ -30,6 +31,8 @@ import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.STRING import com.squareup.kotlinpoet.TypeAliasSpec import com.squareup.kotlinpoet.TypeSpec +import graphql.language.Document +import graphql.language.FragmentDefinition import graphql.language.ObjectTypeDefinition import graphql.language.OperationDefinition import graphql.parser.Parser @@ -63,10 +66,22 @@ class GraphQLClientGenerator( /** * Generate GraphQL clients for the specified queries. */ - fun generate(queries: List): List { + fun generate(queries: List, fragmentsFile: File? = null): List { + val result = mutableListOf() + + val fragmentsDocument = fragmentsFile?.let { + val fragmentsConst = fragmentsFile.readText().trim() + val d = documentParser.parseDocument(fragmentsConst, parserOptions) + d.definitions.forEach { + if (it !is FragmentDefinition) { + throw InvalidFragmentFileException(fragmentsFile) + } + } + d + } for (query in queries) { - result.addAll(generate(query)) + result.addAll(generate(query, fragmentsFile, fragmentsDocument)) } // common shared types @@ -86,13 +101,13 @@ class GraphQLClientGenerator( } result.add(typeAliasSpec.build()) } - return result + return result.distinctBy { it.packageName + it.name } } /** * Generate GraphQL client wrapper class and data classes that match the specified query. */ - internal fun generate(queryFile: File): List { + internal fun generate(queryFile: File, fragmentsFile: File?, fragmentsDocument: Document?): List { val queryConst = queryFile.readText().trim() val queryDocument = documentParser.parseDocument(queryConst, parserOptions) @@ -113,12 +128,18 @@ class GraphQLClientGenerator( allowDeprecated = config.allowDeprecated, customScalarMap = config.customScalarMap, serializer = config.serializer, - useOptionalInputWrapper = config.useOptionalInputWrapper + useOptionalInputWrapper = config.useOptionalInputWrapper, + fragmentsFile = fragmentsDocument ) val queryConstName = capitalizedOperationName.toUpperUnderscore() val queryConstProp = PropertySpec.builder(queryConstName, STRING) .addModifiers(KModifier.CONST) - .initializer("%S", queryConst) + .initializer( + "%S", + fragmentsFile?.let { + queryConst + "\n\n" + fragmentsFile.readText().trim() // todo only add relevant fragments + } ?: queryConst + ) .build() operationFileSpec.addProperty(queryConstProp) diff --git a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGeneratorContext.kt b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGeneratorContext.kt index bef5742b93..e1bde99e08 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGeneratorContext.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGeneratorContext.kt @@ -36,7 +36,8 @@ data class GraphQLClientGeneratorContext( val allowDeprecated: Boolean = false, val customScalarMap: Map = mapOf(), val serializer: GraphQLSerializer = GraphQLSerializer.JACKSON, - val useOptionalInputWrapper: Boolean = false + val useOptionalInputWrapper: Boolean = false, + val fragmentsFile: Document? = null, ) { // per operation caches val typeSpecs: MutableMap = mutableMapOf() diff --git a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/exceptions/InvalidFragmentFileException.kt b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/exceptions/InvalidFragmentFileException.kt new file mode 100644 index 0000000000..c89ddfe300 --- /dev/null +++ b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/exceptions/InvalidFragmentFileException.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Expedia, 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 + * + * https://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.expediagroup.graphql.plugin.client.generator.exceptions + +import java.io.File + +/** + * Exception thrown when attempting to generate a client with a fragments file containing not allowed definitions. + */ +internal class InvalidFragmentFileException(queryFile: File) : + RuntimeException("GraphQL client does not support a fragments file with other definitions than fragments, ${queryFile.name}") diff --git a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/extensions/DocumentExtensions.kt b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/extensions/DocumentExtensions.kt index 4ea786fbf4..a92e6520ac 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/extensions/DocumentExtensions.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/extensions/DocumentExtensions.kt @@ -22,6 +22,13 @@ import graphql.language.Document import graphql.language.FragmentDefinition internal fun Document.findFragmentDefinition(context: GraphQLClientGeneratorContext, targetFragment: String, targetType: String): FragmentDefinition = + findFragmentDefinitionNullable(context, targetFragment, targetType) + ?: throw InvalidFragmentException(context.operationName, targetFragment, targetType) + +internal fun Document.findFragmentDefinitionNullable(context: GraphQLClientGeneratorContext, targetFragment: String, targetType: String): FragmentDefinition? = this.getDefinitionsOfType(FragmentDefinition::class.java) .find { it.name == targetFragment && context.graphQLSchema.getType(it.typeCondition.name).isPresent } - ?: throw InvalidFragmentException(context.operationName, targetFragment, targetType) + +internal fun GraphQLClientGeneratorContext.findFragmentDefinition(targetFragment: String, targetType: String): FragmentDefinition { + return this.fragmentsFile?.findFragmentDefinitionNullable(this, targetFragment, targetType) ?: this.queryDocument.findFragmentDefinition(this, targetFragment, targetType) +} diff --git a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateGraphQLObjectTypeSpec.kt b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateGraphQLObjectTypeSpec.kt index 23d5b3dd79..8308b26542 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateGraphQLObjectTypeSpec.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateGraphQLObjectTypeSpec.kt @@ -78,8 +78,8 @@ internal fun generateGraphQLObjectTypeSpec( selectionSet.getSelectionsOfType(FragmentSpread::class.java) .forEach { fragment -> - val fragmentDefinition = context.queryDocument - .findFragmentDefinition(context, fragment.name, objectDefinition.name) + val fragmentDefinition = context + .findFragmentDefinition(fragment.name, objectDefinition.name) generatePropertySpecs( context = context, objectName = objectDefinition.name, diff --git a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateTypeName.kt b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateTypeName.kt index b74b4fca92..a56cd9c808 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateTypeName.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateTypeName.kt @@ -21,6 +21,7 @@ import com.expediagroup.graphql.plugin.client.generator.GraphQLSerializer import com.expediagroup.graphql.plugin.client.generator.ScalarConverterInfo import com.expediagroup.graphql.plugin.client.generator.exceptions.UnknownGraphQLTypeException import com.expediagroup.graphql.plugin.client.generator.extensions.findFragmentDefinition +import com.expediagroup.graphql.plugin.client.generator.extensions.findFragmentDefinitionNullable import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.BOOLEAN import com.squareup.kotlinpoet.ClassName @@ -68,6 +69,7 @@ internal fun generateTypeName( Scalars.GraphQLBoolean.name -> BOOLEAN else -> generateCustomClassName(context, graphQLType, selectionSet) } + is ListType -> { val type = generateTypeName(context, graphQLType.type, selectionSet) val parameterizedType = if (context.serializer == GraphQLSerializer.KOTLINX && context.isCustomScalar(type)) { @@ -114,24 +116,29 @@ internal fun generateCustomClassName(context: GraphQLClientGeneratorContext, gra className = generateClassName(context, graphQLTypeDefinition, selectionSet) context.typeSpecs[className] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet) } + is InputObjectTypeDefinition -> { className = generateClassName(context, graphQLTypeDefinition, selectionSet, packageName = "${context.packageName}.inputs") context.inputClassToTypeSpecs[className] = generateGraphQLInputObjectTypeSpec(context, graphQLTypeDefinition) } + is EnumTypeDefinition -> { className = generateClassName(context, graphQLTypeDefinition, selectionSet, packageName = "${context.packageName}.enums") context.enumClassToTypeSpecs[className] = generateGraphQLEnumTypeSpec(context, graphQLTypeDefinition) } + is InterfaceTypeDefinition -> { className = generateClassName(context, graphQLTypeDefinition, selectionSet) context.polymorphicTypes[className] = mutableListOf(className) context.typeSpecs[className] = generateGraphQLInterfaceTypeSpec(context, graphQLTypeDefinition, selectionSet) } + is UnionTypeDefinition -> { className = generateClassName(context, graphQLTypeDefinition, selectionSet) context.polymorphicTypes[className] = mutableListOf(className) context.typeSpecs[className] = generateGraphQLUnionTypeSpec(context, graphQLTypeDefinition, selectionSet) } + is ScalarTypeDefinition -> { // its not possible to enter this clause if converter is not available val graphQLScalarMapping = context.customScalarMap[graphQLTypeName]!! @@ -167,6 +174,7 @@ internal fun generateCustomClassName(context: GraphQLClientGeneratorContext, gra context.polymorphicTypes[className] = mutableListOf(className) generateGraphQLInterfaceTypeSpec(context, graphQLTypeDefinition, selectionSet, overriddenName) } + is UnionTypeDefinition -> { context.polymorphicTypes[className] = mutableListOf(className) generateGraphQLUnionTypeSpec(context, graphQLTypeDefinition, selectionSet, overriddenName) @@ -189,8 +197,18 @@ internal fun generateClassName( nameOverride: String? = null, packageName: String = "${context.packageName}.${context.operationName.lowercase()}" ): ClassName { + var pName = packageName + selectionSet?.let { + if (it.selections.size == 1 && it.selections.first() is FragmentSpread) { + val first = it.selections.first() as FragmentSpread + val isSharedFragment = context.fragmentsFile?.findFragmentDefinitionNullable(context, first.name, nameOverride ?: graphQLType.name) != null + if (isSharedFragment) { + pName = "${context.packageName}.fragments" + } + } + } val typeName = nameOverride ?: graphQLType.name - val className = ClassName(packageName, typeName) + val className = ClassName(pName, typeName) val classNames = context.classNameCache.getOrDefault(graphQLType.name, mutableListOf()) classNames.add(className) context.classNameCache[graphQLType.name] = classNames @@ -235,6 +253,7 @@ private fun calculateSelectedFields( result.addAll(calculateSelectedFields(context, targetType, selection.selectionSet, "$path$fieldName.")) } } + is InlineFragment -> { val targetFragmentType = selection.typeCondition.name val fragmentPathPrefix = if (targetFragmentType == targetType) { @@ -244,8 +263,10 @@ private fun calculateSelectedFields( } result.addAll(calculateSelectedFields(context, targetType, selection.selectionSet, fragmentPathPrefix)) } + is FragmentSpread -> { - val fragmentDefinition = context.queryDocument.findFragmentDefinition(context, selection.name, targetType) + val fragmentDefinition = context.findFragmentDefinition(selection.name, targetType) + val targetFragmentType = fragmentDefinition.typeCondition.name val fragmentPathPrefix = if (targetFragmentType == targetType) { path diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/Fragments.graphql b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/Fragments.graphql new file mode 100644 index 0000000000..bcc2fa6645 --- /dev/null +++ b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/Fragments.graphql @@ -0,0 +1,11 @@ +fragment complexObjectFields on ComplexObject { + id + name + details { + ...detailObjectFields + } +} + +fragment detailObjectFields on DetailsObject { + value +} diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/ObjectWithNamedFragmentQuery.graphql b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/ObjectWithNamedFragmentQuery.graphql new file mode 100644 index 0000000000..bcbd67f2fb --- /dev/null +++ b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/ObjectWithNamedFragmentQuery.graphql @@ -0,0 +1,5 @@ +query ObjectWithNamedFragmentQuery { + complexObjectQuery { + ...complexObjectFields + } +} diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/ObjectWithNamedFragmentQuery.kt b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/ObjectWithNamedFragmentQuery.kt new file mode 100644 index 0000000000..1d19fba253 --- /dev/null +++ b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/ObjectWithNamedFragmentQuery.kt @@ -0,0 +1,29 @@ +package com.expediagroup.graphql.generated + +import com.expediagroup.graphql.client.Generated +import com.expediagroup.graphql.client.types.GraphQLClientRequest +import com.expediagroup.graphql.generated.fragments.ComplexObject +import kotlin.String +import kotlin.reflect.KClass + +public const val OBJECT_WITH_NAMED_FRAGMENT_QUERY: String = + "query ObjectWithNamedFragmentQuery {\n complexObjectQuery {\n ...complexObjectFields\n }\n}\n\nfragment complexObjectFields on ComplexObject {\n id\n name\n details {\n ...detailObjectFields\n }\n}\n\nfragment detailObjectFields on DetailsObject {\n value\n}" + +@Generated +public class ObjectWithNamedFragmentQuery : + GraphQLClientRequest { + override val query: String = OBJECT_WITH_NAMED_FRAGMENT_QUERY + + override val operationName: String = "ObjectWithNamedFragmentQuery" + + override fun responseType(): KClass = + ObjectWithNamedFragmentQuery.Result::class + + @Generated + public data class Result( + /** + * Query returning an object that references another object + */ + public val complexObjectQuery: ComplexObject, + ) +} diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/ObjectWithNamedFragmentQuery2.graphql b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/ObjectWithNamedFragmentQuery2.graphql new file mode 100644 index 0000000000..089991807d --- /dev/null +++ b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/ObjectWithNamedFragmentQuery2.graphql @@ -0,0 +1,7 @@ +query ObjectWithNamedFragmentQuery2 { + complexObjectQuery { + ...complexObjectFields + } +} + + diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/ObjectWithNamedFragmentQuery2.kt b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/ObjectWithNamedFragmentQuery2.kt new file mode 100644 index 0000000000..17a54deab5 --- /dev/null +++ b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/ObjectWithNamedFragmentQuery2.kt @@ -0,0 +1,29 @@ +package com.expediagroup.graphql.generated + +import com.expediagroup.graphql.client.Generated +import com.expediagroup.graphql.client.types.GraphQLClientRequest +import com.expediagroup.graphql.generated.fragments.ComplexObject +import kotlin.String +import kotlin.reflect.KClass + +public const val OBJECT_WITH_NAMED_FRAGMENT_QUERY2: String = + "query ObjectWithNamedFragmentQuery2 {\n complexObjectQuery {\n ...complexObjectFields\n }\n}\n\nfragment complexObjectFields on ComplexObject {\n id\n name\n details {\n ...detailObjectFields\n }\n}\n\nfragment detailObjectFields on DetailsObject {\n value\n}" + +@Generated +public class ObjectWithNamedFragmentQuery2 : + GraphQLClientRequest { + override val query: String = OBJECT_WITH_NAMED_FRAGMENT_QUERY2 + + override val operationName: String = "ObjectWithNamedFragmentQuery2" + + override fun responseType(): KClass = + ObjectWithNamedFragmentQuery2.Result::class + + @Generated + public data class Result( + /** + * Query returning an object that references another object + */ + public val complexObjectQuery: ComplexObject, + ) +} diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/fragments/ComplexObject.kt b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/fragments/ComplexObject.kt new file mode 100644 index 0000000000..f3862a7f10 --- /dev/null +++ b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/fragments/ComplexObject.kt @@ -0,0 +1,26 @@ +package com.expediagroup.graphql.generated.fragments + +import com.expediagroup.graphql.client.Generated +import kotlin.Int +import kotlin.String + +/** + * Multi line description of a complex type. + * This is a second line of the paragraph. + * This is final line of the description. + */ +@Generated +public data class ComplexObject( + /** + * Some unique identifier + */ + public val id: Int, + /** + * Some object name + */ + public val name: String, + /** + * Some additional details + */ + public val details: DetailsObject, +) diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/fragments/DetailsObject.kt b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/fragments/DetailsObject.kt new file mode 100644 index 0000000000..926328dd65 --- /dev/null +++ b/plugins/client/graphql-kotlin-client-generator/src/test/data/fragments/object_named_fragments/fragments/DetailsObject.kt @@ -0,0 +1,15 @@ +package com.expediagroup.graphql.generated.fragments + +import com.expediagroup.graphql.client.Generated +import kotlin.String + +/** + * Inner type object description + */ +@Generated +public data class DetailsObject( + /** + * Actual detail value + */ + public val `value`: String, +) diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/kotlin/com/expediagroup/graphql/plugin/client/generator/GenerateGraphQLClientITFragments.kt b/plugins/client/graphql-kotlin-client-generator/src/test/kotlin/com/expediagroup/graphql/plugin/client/generator/GenerateGraphQLClientITFragments.kt new file mode 100755 index 0000000000..8c1e2d7b17 --- /dev/null +++ b/plugins/client/graphql-kotlin-client-generator/src/test/kotlin/com/expediagroup/graphql/plugin/client/generator/GenerateGraphQLClientITFragments.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2021 Expedia, 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 + * + * https://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.expediagroup.graphql.plugin.client.generator + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.io.File + +class GenerateGraphQLClientITFragments { + + @ParameterizedTest + @MethodSource("generatorTests") + fun `verify generation of client code using default settings`(testDirectory: File) { + verifyClientGeneration(defaultConfig, testDirectory, File("src/test/data/fragments/Fragments.graphql")) + } + + companion object { + @JvmStatic + fun generatorTests(): List = locateTestCaseArguments("src/test/data/fragments") + } +} diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLTestUtils.kt b/plugins/client/graphql-kotlin-client-generator/src/test/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLTestUtils.kt index 44929219c3..afcf019d25 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/test/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLTestUtils.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/test/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLTestUtils.kt @@ -56,11 +56,11 @@ internal fun locateTestFiles(directory: File): Pair, Map**Default value is:** `GraphQLSerializer.JACKSON`. | | `schemaFile` | File | yes | GraphQL schema file that will be used to generate client code. | | `useOptionalInputWrapper` | Boolean | | Boolean opt-in flag to wrap nullable arguments in `OptionalInput` that distinguish between `null` and undefined/omitted value.
**Default value is:** `false`.
**Command line property is**: `useOptionalInputWrapper` | +| `queryFragmentsFile` | File | no | Shared fragements which can be used in queries. | ### graphqLGraalVmMetadata diff --git a/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/actions/GenerateClientAction.kt b/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/actions/GenerateClientAction.kt index bba2e0b618..1ac6eb4678 100644 --- a/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/actions/GenerateClientAction.kt +++ b/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/actions/GenerateClientAction.kt @@ -36,6 +36,7 @@ abstract class GenerateClientAction : WorkAction { val serializer = GraphQLSerializer.valueOf(parameters.serializer.get().name) val schemaPath = parameters.schemaPath.get() val queryFiles = parameters.queryFiles.get() + val queryFragmentsFile = parameters.queryFragmentsFile.orNull val targetDirectory = parameters.targetDirectory.get() val useOptionalInputWrapper = parameters.useOptionalInputWrapper.get() val parserOptions = parameters.parserOptions.get() @@ -54,7 +55,8 @@ abstract class GenerateClientAction : WorkAction { parserOptions.captureIgnoredChars?.let { captureIgnoredChars(it) } parserOptions.captureSourceLocation?.let { captureSourceLocation(it) } parserOptions.captureLineComments?.let { captureLineComments(it) } - } + }, + fragmentsFile = queryFragmentsFile ).forEach { it.writeTo(targetDirectory) } diff --git a/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/parameters/GenerateClientParameters.kt b/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/parameters/GenerateClientParameters.kt index a118fffe42..32c2e2f1c7 100644 --- a/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/parameters/GenerateClientParameters.kt +++ b/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/parameters/GenerateClientParameters.kt @@ -40,6 +40,8 @@ interface GenerateClientParameters : WorkParameters { val schemaPath: Property /** List of query files that will be processed to generate HTTP clients. */ val queryFiles: ListProperty + /** Fragments file containing GraphQL Fragments to share between queries. */ + val queryFragmentsFile: Property /** Directory where to save the generated source files. */ val targetDirectory: Property /** Explicit opt-in flag to wrap nullable arguments in OptionalInput that supports both null and undefined values. */ diff --git a/plugins/graphql-kotlin-maven-plugin/README.md b/plugins/graphql-kotlin-maven-plugin/README.md index 8ea6548f39..96608c143b 100755 --- a/plugins/graphql-kotlin-maven-plugin/README.md +++ b/plugins/graphql-kotlin-maven-plugin/README.md @@ -115,6 +115,7 @@ Generate GraphQL client code based on the provided GraphQL schema and target que | `serializer` | GraphQLSerializer | | JSON serializer that will be used to generate the data classes.
**Default value is:** `GraphQLSerializer.JACKSON`. | | `schemaFile` | String | | GraphQL schema file that will be used to generate client code.
**Default value is**: `${project.build.directory}/schema.graphql`
**User property is**: `graphql.schemaFile`. | | `useOptionalInputWrapper` | Boolean | | Boolean opt-in flag to wrap nullable arguments in `OptionalInput` that distinguish between `null` and undefined/omitted value.
**Default value is:** `false`.
**User property is**: `graphql.useOptionalInputWrapper` | +| `queryFragmentsFile` | File | no | Shared fragements which can be used in queries. | **Parameter Details** diff --git a/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/GenerateClientAbstractMojo.kt b/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/GenerateClientAbstractMojo.kt index e77b039ce5..49319327e9 100644 --- a/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/GenerateClientAbstractMojo.kt +++ b/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/GenerateClientAbstractMojo.kt @@ -103,6 +103,12 @@ abstract class GenerateClientAbstractMojo : AbstractMojo() { @Parameter(name = "queryFiles") private var queryFiles: List? = null + /** + * Fragments file containing GraphQL Fragments to share between queries. + */ + @Parameter(name = "queryFragmentsFile") + private var queryFragmentsFile: File? = null + /** * JSON serializer that will be used to generate the data classes.. */ @@ -141,7 +147,7 @@ abstract class GenerateClientAbstractMojo : AbstractMojo() { captureLineComments?.let { captureLineComments(it) } captureSourceLocation?.let { captureSourceLocation(it) } } - }).forEach { + }, fragmentsFile = queryFragmentsFile).forEach { it.writeTo(outputDirectory) } diff --git a/website/docs/plugins/gradle-plugin-usage-client.mdx b/website/docs/plugins/gradle-plugin-usage-client.mdx index 8e33db432e..708b675439 100644 --- a/website/docs/plugins/gradle-plugin-usage-client.mdx +++ b/website/docs/plugins/gradle-plugin-usage-client.mdx @@ -475,6 +475,7 @@ graphql { headers = mapOf("X-Custom-Header" to "My-Custom-Header") queryFiles = listOf(file("${project.projectDir}/src/main/resources/queries/MyQuery.graphql")) serializer = GraphQLSerializer.KOTLINX + queryFragmentsFile = file("${project.projectDir}/src/main/resources/fragments.graphql") timeout { connect = 10_000 read = 30_000 @@ -510,7 +511,7 @@ val graphqlGenerateClient by tasks.getting(GraphQLGenerateClientTask::class) { customScalars.add(GraphQLScalar("UUID", "java.util.UUID", "com.example.UUIDScalarConverter")) queryFiles.from("${project.projectDir}/src/main/resources/queries/MyQuery.graphql") serializer.set(GraphQLSerializer.KOTLINX) - + queryFragmentsFile.from("${project.projectDir}/src/main/resources/fragments.graphql") dependsOn("graphqlDownloadSDL") } ``` @@ -537,6 +538,8 @@ graphql { headers = ["X-Custom-Header" : "My-Custom-Header"] queryFiles = [file("${project.projectDir}/src/main/resources/queries/MyQuery.graphql")] serializer = GraphQLSerializer.KOTLINX + queryFragmentsFile = file("${project.projectDir}/src/main/resources/fragments.graphql") + timeout { t -> t.connect = 10000 t.read = 30000 @@ -570,6 +573,7 @@ graphqlGenerateClient { customScalars.add(new GraphQLScalar("UUID", "java.util.UUID", "com.example.UUIDScalarConverter")) queryFiles.from("${project.projectDir}/src/main/resources/queries/MyQuery.graphql") serializer = GraphQLSerializer.KOTLINX + queryFragmentsFile.from("${project.projectDir}/src/main/resources/fragments.graphql") dependsOn graphqlDownloadSDL } diff --git a/website/docs/plugins/maven-plugin-usage-client.md b/website/docs/plugins/maven-plugin-usage-client.md index d91a209ba1..d79262ebf8 100644 --- a/website/docs/plugins/maven-plugin-usage-client.md +++ b/website/docs/plugins/maven-plugin-usage-client.md @@ -403,6 +403,7 @@ the GraphQL client data models using `kotlinx.serialization` that are based on t ${project.basedir}/src/main/resources/queries/MyQuery.graphql KOTLINX + ${project.basedir}/src/main/resources/fragments.graphql