Skip to content

Commit

Permalink
basic support for a fragment file
Browse files Browse the repository at this point in the history
  • Loading branch information
RobpMobX authored and Robert Pospisil committed Nov 22, 2023
1 parent 716809e commit eaa624d
Show file tree
Hide file tree
Showing 24 changed files with 271 additions and 20 deletions.
2 changes: 1 addition & 1 deletion plugins/client/graphql-kotlin-client-generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ fun generateClient(
schemaPath: String,
queries: List<File>,
useOptionalInputWrapper: Boolean = false,
parserOptions: ParserOptions.Builder.() -> Unit = {}
parserOptions: ParserOptions.Builder.() -> Unit = {},
fragmentsFile: File?
): List<FileSpec> {
val customScalars = customScalarsMap.associateBy { it.scalar }
val config = GraphQLClientGeneratorConfig(
Expand All @@ -47,5 +48,5 @@ fun generateClient(
parserOptions = parserOptions
)
val generator = GraphQLClientGenerator(schemaPath, config)
return generator.generate(queries)
return generator.generate(queries, fragmentsFile)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -63,10 +66,22 @@ class GraphQLClientGenerator(
/**
* Generate GraphQL clients for the specified queries.
*/
fun generate(queries: List<File>): List<FileSpec> {
fun generate(queries: List<File>, fragmentsFile: File? = null): List<FileSpec> {

val result = mutableListOf<FileSpec>()

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
Expand All @@ -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<FileSpec> {
internal fun generate(queryFile: File, fragmentsFile: File?, fragmentsDocument: Document?): List<FileSpec> {
val queryConst = queryFile.readText().trim()
val queryDocument = documentParser.parseDocument(queryConst, parserOptions)

Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ data class GraphQLClientGeneratorContext(
val allowDeprecated: Boolean = false,
val customScalarMap: Map<String, GraphQLScalar> = mapOf(),
val serializer: GraphQLSerializer = GraphQLSerializer.JACKSON,
val useOptionalInputWrapper: Boolean = false
val useOptionalInputWrapper: Boolean = false,
val fragmentsFile: Document? = null,
) {
// per operation caches
val typeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
Expand Down
Original file line number Diff line number Diff line change
@@ -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}")
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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]!!
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
fragment complexObjectFields on ComplexObject {
id
name
details {
...detailObjectFields
}
}

fragment detailObjectFields on DetailsObject {
value
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query ObjectWithNamedFragmentQuery {
complexObjectQuery {
...complexObjectFields
}
}
Original file line number Diff line number Diff line change
@@ -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<ObjectWithNamedFragmentQuery.Result> {
override val query: String = OBJECT_WITH_NAMED_FRAGMENT_QUERY

override val operationName: String = "ObjectWithNamedFragmentQuery"

override fun responseType(): KClass<ObjectWithNamedFragmentQuery.Result> =
ObjectWithNamedFragmentQuery.Result::class

@Generated
public data class Result(
/**
* Query returning an object that references another object
*/
public val complexObjectQuery: ComplexObject,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
query ObjectWithNamedFragmentQuery2 {
complexObjectQuery {
...complexObjectFields
}
}


Original file line number Diff line number Diff line change
@@ -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<ObjectWithNamedFragmentQuery2.Result> {
override val query: String = OBJECT_WITH_NAMED_FRAGMENT_QUERY2

override val operationName: String = "ObjectWithNamedFragmentQuery2"

override fun responseType(): KClass<ObjectWithNamedFragmentQuery2.Result> =
ObjectWithNamedFragmentQuery2.Result::class

@Generated
public data class Result(
/**
* Query returning an object that references another object
*/
public val complexObjectQuery: ComplexObject,
)
}
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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,
)
Loading

0 comments on commit eaa624d

Please sign in to comment.