Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = {},
useSharedResponseTypes: Boolean = false
): List<FileSpec> {
val customScalars = customScalarsMap.associateBy { it.scalar }
val config = GraphQLClientGeneratorConfig(
Expand All @@ -44,7 +45,8 @@ fun generateClient(
customScalarMap = customScalars,
serializer = serializer,
useOptionalInputWrapper = useOptionalInputWrapper,
parserOptions = parserOptions
parserOptions = parserOptions,
useSharedResponseTypes = useSharedResponseTypes
)
val generator = GraphQLClientGenerator(schemaPath, config)
return generator.generate(queries)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class GraphQLClientGenerator(
*/
fun generate(queries: List<File>): List<FileSpec> {
val result = mutableListOf<FileSpec>()

// Generate client code with shared types
for (query in queries) {
result.addAll(generate(query))
}
Expand Down Expand Up @@ -119,8 +121,10 @@ class GraphQLClientGenerator(
allowDeprecated = config.allowDeprecated,
customScalarMap = config.customScalarMap,
serializer = config.serializer,
useOptionalInputWrapper = config.useOptionalInputWrapper
useOptionalInputWrapper = config.useOptionalInputWrapper,
config = config
)

val queryConstName = capitalizedOperationName.toUpperUnderscore()
val queryConstProp = PropertySpec.builder(queryConstName, STRING)
.addModifiers(KModifier.CONST)
Expand Down Expand Up @@ -216,6 +220,7 @@ class GraphQLClientGenerator(
// shared types
sharedTypes.putAll(context.enumClassToTypeSpecs.mapValues { listOf(it.value) })
sharedTypes.putAll(context.inputClassToTypeSpecs.mapValues { listOf(it.value) })
sharedTypes.putAll(context.responseClassToTypeSpecs.mapValues { listOf(it.value) })
context.scalarClassToConverterTypeSpecs
.values
.forEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ data class GraphQLClientGeneratorConfig(
val serializer: GraphQLSerializer = GraphQLSerializer.JACKSON,
/** Explicit opt-in flag to enable support for optional inputs. */
val useOptionalInputWrapper: Boolean = false,
/** Boolean flag indicating whether to generate shared response types instead of operation-specific duplicates. Defaults to false. */
val useSharedResponseTypes: Boolean = false,
/** Set parser options for processing GraphQL queries and schema definition language documents */
val parserOptions: ParserOptions.Builder.() -> Unit = {}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.squareup.kotlinpoet.TypeAliasSpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
import graphql.language.Document
import graphql.language.Selection
import graphql.schema.idl.TypeDefinitionRegistry

/**
Expand All @@ -36,7 +37,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 config: GraphQLClientGeneratorConfig
) {
// per operation caches
val typeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
Expand All @@ -45,13 +47,16 @@ data class GraphQLClientGeneratorContext(
// shared type caches
val enumClassToTypeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
val inputClassToTypeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
val responseClassToTypeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
val scalarClassToConverterTypeSpecs: MutableMap<ClassName, ScalarConverterInfo> = mutableMapOf()
val typeAliases: MutableMap<String, TypeAliasSpec> = mutableMapOf()
internal fun isTypeAlias(typeName: String) = typeAliases.containsKey(typeName)

// class name and type selection caches
val classNameCache: MutableMap<String, MutableList<ClassName>> = mutableMapOf()
val typeToSelectionSetMap: MutableMap<String, Set<String>> = mutableMapOf()
val responseTypeToSelectionSetMap: MutableMap<String, MutableSet<Selection<*>>> = mutableMapOf()
val sharedTypeVariantToSelectionSetMap: MutableMap<String, Set<String>> = mutableMapOf()

private val customScalarClassNames: Set<ClassName> = customScalarMap.values.map { it.className }.toSet()
internal fun isCustomScalar(typeName: TypeName): Boolean = customScalarClassNames.contains(typeName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,50 @@ internal fun generateCustomClassName(context: GraphQLClientGeneratorContext, gra
// generate corresponding type spec
when (graphQLTypeDefinition) {
is ObjectTypeDefinition -> {
className = generateClassName(context, graphQLTypeDefinition, selectionSet)
context.typeSpecs[className] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet)
if (context.config.useSharedResponseTypes) {
// Use cross-operation reuse logic similar to existing single-operation logic
val globalCachedTypes = context.responseClassToTypeSpecs.keys.filter { it.simpleName.startsWith(graphQLTypeDefinition.name) }

if (globalCachedTypes.isNotEmpty()) {
// Check if any existing shared type matches this selection set
var foundMatch = false
for (cachedType in globalCachedTypes) {
if (isCachedTypeApplicableForSharedType(context, cachedType, graphQLTypeDefinition, selectionSet)) {
className = cachedType
foundMatch = true
break
}
}

if (!foundMatch) {
// Generate new variant (ComplexObject2, ComplexObject3, etc.)
val variantNumber = globalCachedTypes.size + 1
val variantName = if (variantNumber == 1) graphQLTypeDefinition.name else "${graphQLTypeDefinition.name}$variantNumber"
className = ClassName("${context.packageName}.responses", variantName)
context.responseClassToTypeSpecs[className] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet, variantName)

// Track selection set for this variant
if (selectionSet != null) {
val selectedFields = calculateSelectedFields(context, graphQLTypeDefinition.name, selectionSet)
context.sharedTypeVariantToSelectionSetMap[variantName] = selectedFields
}
}
} else {
// First occurrence - create base shared type
className = ClassName("${context.packageName}.responses", graphQLTypeDefinition.name)
context.responseClassToTypeSpecs[className] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet)

// Track selection set for this variant
if (selectionSet != null) {
val selectedFields = calculateSelectedFields(context, graphQLTypeDefinition.name, selectionSet)
context.sharedTypeVariantToSelectionSetMap[graphQLTypeDefinition.name] = selectedFields
}
}
} else {
// Use original logic for operation-specific types
className = generateClassName(context, graphQLTypeDefinition, selectionSet)
context.typeSpecs[className] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet)
}
}
is InputObjectTypeDefinition -> {
className = generateClassName(context, graphQLTypeDefinition, selectionSet, packageName = "${context.packageName}.inputs")
Expand Down Expand Up @@ -258,3 +300,19 @@ private fun calculateSelectedFields(
}
return result
}

/**
* Helper function to check if a cached shared type matches the current selection set.
*/
private fun isCachedTypeApplicableForSharedType(
context: GraphQLClientGeneratorContext,
cachedClassName: ClassName,
graphQLTypeDefinition: TypeDefinition<*>,
selectionSet: SelectionSet?
): Boolean {
if (selectionSet == null) return true

val selectedFields = calculateSelectedFields(context, graphQLTypeDefinition.name, selectionSet)
val cachedTypeFields = context.sharedTypeVariantToSelectionSetMap[cachedClassName.simpleName]
return selectedFields == cachedTypeFields
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
query Operation1 {
first: complexObjectQuery {
id
name
}
second: complexObjectQuery {
id
name
details {
id
value
}
}
third: complexObjectQuery {
id
name
details {
id
}
}
fourth: complexObjectQuery {
id
name
}
fifth: complexObjectQuery {
id
name
details {
id
value
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.expediagroup.graphql.generated

import com.expediagroup.graphql.client.Generated
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import com.expediagroup.graphql.generated.responses.ComplexObject
import com.expediagroup.graphql.generated.responses.ComplexObject2
import com.expediagroup.graphql.generated.responses.ComplexObject3
import com.fasterxml.jackson.`annotation`.JsonProperty
import kotlin.String
import kotlin.reflect.KClass

public const val OPERATION1: String =
"query Operation1 {\n first: complexObjectQuery {\n id\n name\n }\n second: complexObjectQuery {\n id\n name\n details {\n id\n value\n }\n }\n third: complexObjectQuery {\n id\n name\n details {\n id\n }\n }\n fourth: complexObjectQuery {\n id\n name\n }\n fifth: complexObjectQuery {\n id\n name\n details {\n id\n value\n }\n }\n}"

@Generated
public class Operation1 : GraphQLClientRequest<Operation1.Result> {
override val query: String = OPERATION1

override val operationName: String = "Operation1"

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

@Generated
public data class Result(
/**
* Query returning an object that references another object
*/
@get:JsonProperty(value = "first")
public val first: ComplexObject,
/**
* Query returning an object that references another object
*/
@get:JsonProperty(value = "second")
public val second: ComplexObject2,
/**
* Query returning an object that references another object
*/
@get:JsonProperty(value = "third")
public val third: ComplexObject3,
/**
* Query returning an object that references another object
*/
@get:JsonProperty(value = "fourth")
public val fourth: ComplexObject,
/**
* Query returning an object that references another object
*/
@get:JsonProperty(value = "fifth")
public val fifth: ComplexObject2,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
query Operation2 {
first: complexObjectQuery {
id
name
}
second: complexObjectQuery {
id
name
details {
id
value
}
}
third: complexObjectQuery {
id
name
details {
id
}
}
fourth: complexObjectQuery {
id
name
}
fifth: complexObjectQuery {
id
name
details {
id
value
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.expediagroup.graphql.generated

import com.expediagroup.graphql.client.Generated
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import com.expediagroup.graphql.generated.responses.ComplexObject
import com.expediagroup.graphql.generated.responses.ComplexObject2
import com.expediagroup.graphql.generated.responses.ComplexObject3
import com.fasterxml.jackson.`annotation`.JsonProperty
import kotlin.String
import kotlin.reflect.KClass

public const val OPERATION2: String =
"query Operation2 {\n first: complexObjectQuery {\n id\n name\n }\n second: complexObjectQuery {\n id\n name\n details {\n id\n value\n }\n }\n third: complexObjectQuery {\n id\n name\n details {\n id\n }\n }\n fourth: complexObjectQuery {\n id\n name\n }\n fifth: complexObjectQuery {\n id\n name\n details {\n id\n value\n }\n }\n}"

@Generated
public class Operation2 : GraphQLClientRequest<Operation2.Result> {
override val query: String = OPERATION2

override val operationName: String = "Operation2"

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

@Generated
public data class Result(
/**
* Query returning an object that references another object
*/
@get:JsonProperty(value = "first")
public val first: ComplexObject,
/**
* Query returning an object that references another object
*/
@get:JsonProperty(value = "second")
public val second: ComplexObject2,
/**
* Query returning an object that references another object
*/
@get:JsonProperty(value = "third")
public val third: ComplexObject3,
/**
* Query returning an object that references another object
*/
@get:JsonProperty(value = "fourth")
public val fourth: ComplexObject,
/**
* Query returning an object that references another object
*/
@get:JsonProperty(value = "fifth")
public val fifth: ComplexObject2,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.expediagroup.graphql.generated.responses

import com.expediagroup.graphql.client.Generated
import com.fasterxml.jackson.`annotation`.JsonProperty
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
*/
@get:JsonProperty(value = "id")
public val id: Int,
/**
* Some object name
*/
@get:JsonProperty(value = "name")
public val name: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.expediagroup.graphql.generated.responses

import com.expediagroup.graphql.client.Generated
import com.fasterxml.jackson.`annotation`.JsonProperty
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 ComplexObject2(
/**
* Some unique identifier
*/
@get:JsonProperty(value = "id")
public val id: Int,
/**
* Some object name
*/
@get:JsonProperty(value = "name")
public val name: String,
/**
* Some additional details
*/
@get:JsonProperty(value = "details")
public val details: DetailsObject,
)
Loading
Loading