Skip to content

Commit 2959ce9

Browse files
Devin/1758753881 shared response types (#2136)
### 📝 Description ### 🔗 Related Issues arthurkkp-cog#7 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent e3b74bf commit 2959ce9

File tree

17 files changed

+380
-9
lines changed

17 files changed

+380
-9
lines changed

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generateClient.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ fun generateClient(
3535
schemaPath: String,
3636
queries: List<File>,
3737
useOptionalInputWrapper: Boolean = false,
38-
parserOptions: ParserOptions.Builder.() -> Unit = {}
38+
parserOptions: ParserOptions.Builder.() -> Unit = {},
39+
useSharedResponseTypes: Boolean = false
3940
): List<FileSpec> {
4041
val customScalars = customScalarsMap.associateBy { it.scalar }
4142
val config = GraphQLClientGeneratorConfig(
@@ -44,7 +45,8 @@ fun generateClient(
4445
customScalarMap = customScalars,
4546
serializer = serializer,
4647
useOptionalInputWrapper = useOptionalInputWrapper,
47-
parserOptions = parserOptions
48+
parserOptions = parserOptions,
49+
useSharedResponseTypes = useSharedResponseTypes
4850
)
4951
val generator = GraphQLClientGenerator(schemaPath, config)
5052
return generator.generate(queries)

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGenerator.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ class GraphQLClientGenerator(
6666
*/
6767
fun generate(queries: List<File>): List<FileSpec> {
6868
val result = mutableListOf<FileSpec>()
69+
70+
// Generate client code with shared types
6971
for (query in queries) {
7072
result.addAll(generate(query))
7173
}
@@ -119,8 +121,10 @@ class GraphQLClientGenerator(
119121
allowDeprecated = config.allowDeprecated,
120122
customScalarMap = config.customScalarMap,
121123
serializer = config.serializer,
122-
useOptionalInputWrapper = config.useOptionalInputWrapper
124+
useOptionalInputWrapper = config.useOptionalInputWrapper,
125+
config = config
123126
)
127+
124128
val queryConstName = capitalizedOperationName.toUpperUnderscore()
125129
val queryConstProp = PropertySpec.builder(queryConstName, STRING)
126130
.addModifiers(KModifier.CONST)
@@ -216,6 +220,7 @@ class GraphQLClientGenerator(
216220
// shared types
217221
sharedTypes.putAll(context.enumClassToTypeSpecs.mapValues { listOf(it.value) })
218222
sharedTypes.putAll(context.inputClassToTypeSpecs.mapValues { listOf(it.value) })
223+
sharedTypes.putAll(context.responseClassToTypeSpecs.mapValues { listOf(it.value) })
219224
context.scalarClassToConverterTypeSpecs
220225
.values
221226
.forEach {

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGeneratorConfig.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ data class GraphQLClientGeneratorConfig(
3333
val serializer: GraphQLSerializer = GraphQLSerializer.JACKSON,
3434
/** Explicit opt-in flag to enable support for optional inputs. */
3535
val useOptionalInputWrapper: Boolean = false,
36+
/** Boolean flag indicating whether to generate shared response types instead of operation-specific duplicates. Defaults to false. */
37+
val useSharedResponseTypes: Boolean = false,
3638
/** Set parser options for processing GraphQL queries and schema definition language documents */
3739
val parserOptions: ParserOptions.Builder.() -> Unit = {}
3840
)

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGeneratorContext.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.squareup.kotlinpoet.TypeAliasSpec
2121
import com.squareup.kotlinpoet.TypeName
2222
import com.squareup.kotlinpoet.TypeSpec
2323
import graphql.language.Document
24+
import graphql.language.Selection
2425
import graphql.schema.idl.TypeDefinitionRegistry
2526

2627
/**
@@ -36,7 +37,8 @@ data class GraphQLClientGeneratorContext(
3637
val allowDeprecated: Boolean = false,
3738
val customScalarMap: Map<String, GraphQLScalar> = mapOf(),
3839
val serializer: GraphQLSerializer = GraphQLSerializer.JACKSON,
39-
val useOptionalInputWrapper: Boolean = false
40+
val useOptionalInputWrapper: Boolean = false,
41+
val config: GraphQLClientGeneratorConfig
4042
) {
4143
// per operation caches
4244
val typeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
@@ -45,13 +47,16 @@ data class GraphQLClientGeneratorContext(
4547
// shared type caches
4648
val enumClassToTypeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
4749
val inputClassToTypeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
50+
val responseClassToTypeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
4851
val scalarClassToConverterTypeSpecs: MutableMap<ClassName, ScalarConverterInfo> = mutableMapOf()
4952
val typeAliases: MutableMap<String, TypeAliasSpec> = mutableMapOf()
5053
internal fun isTypeAlias(typeName: String) = typeAliases.containsKey(typeName)
5154

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

5661
private val customScalarClassNames: Set<ClassName> = customScalarMap.values.map { it.className }.toSet()
5762
internal fun isCustomScalar(typeName: TypeName): Boolean = customScalarClassNames.contains(typeName)

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateTypeName.kt

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,50 @@ internal fun generateCustomClassName(context: GraphQLClientGeneratorContext, gra
111111
// generate corresponding type spec
112112
when (graphQLTypeDefinition) {
113113
is ObjectTypeDefinition -> {
114-
className = generateClassName(context, graphQLTypeDefinition, selectionSet)
115-
context.typeSpecs[className] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet)
114+
if (context.config.useSharedResponseTypes) {
115+
// Use cross-operation reuse logic similar to existing single-operation logic
116+
val globalCachedTypes = context.responseClassToTypeSpecs.keys.filter { it.simpleName.startsWith(graphQLTypeDefinition.name) }
117+
118+
if (globalCachedTypes.isNotEmpty()) {
119+
// Check if any existing shared type matches this selection set
120+
var foundMatch = false
121+
for (cachedType in globalCachedTypes) {
122+
if (isCachedTypeApplicableForSharedType(context, cachedType, graphQLTypeDefinition, selectionSet)) {
123+
className = cachedType
124+
foundMatch = true
125+
break
126+
}
127+
}
128+
129+
if (!foundMatch) {
130+
// Generate new variant (ComplexObject2, ComplexObject3, etc.)
131+
val variantNumber = globalCachedTypes.size + 1
132+
val variantName = if (variantNumber == 1) graphQLTypeDefinition.name else "${graphQLTypeDefinition.name}$variantNumber"
133+
className = ClassName("${context.packageName}.responses", variantName)
134+
context.responseClassToTypeSpecs[className] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet, variantName)
135+
136+
// Track selection set for this variant
137+
if (selectionSet != null) {
138+
val selectedFields = calculateSelectedFields(context, graphQLTypeDefinition.name, selectionSet)
139+
context.sharedTypeVariantToSelectionSetMap[variantName] = selectedFields
140+
}
141+
}
142+
} else {
143+
// First occurrence - create base shared type
144+
className = ClassName("${context.packageName}.responses", graphQLTypeDefinition.name)
145+
context.responseClassToTypeSpecs[className] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet)
146+
147+
// Track selection set for this variant
148+
if (selectionSet != null) {
149+
val selectedFields = calculateSelectedFields(context, graphQLTypeDefinition.name, selectionSet)
150+
context.sharedTypeVariantToSelectionSetMap[graphQLTypeDefinition.name] = selectedFields
151+
}
152+
}
153+
} else {
154+
// Use original logic for operation-specific types
155+
className = generateClassName(context, graphQLTypeDefinition, selectionSet)
156+
context.typeSpecs[className] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet)
157+
}
116158
}
117159
is InputObjectTypeDefinition -> {
118160
className = generateClassName(context, graphQLTypeDefinition, selectionSet, packageName = "${context.packageName}.inputs")
@@ -258,3 +300,19 @@ private fun calculateSelectedFields(
258300
}
259301
return result
260302
}
303+
304+
/**
305+
* Helper function to check if a cached shared type matches the current selection set.
306+
*/
307+
private fun isCachedTypeApplicableForSharedType(
308+
context: GraphQLClientGeneratorContext,
309+
cachedClassName: ClassName,
310+
graphQLTypeDefinition: TypeDefinition<*>,
311+
selectionSet: SelectionSet?
312+
): Boolean {
313+
if (selectionSet == null) return true
314+
315+
val selectedFields = calculateSelectedFields(context, graphQLTypeDefinition.name, selectionSet)
316+
val cachedTypeFields = context.sharedTypeVariantToSelectionSetMap[cachedClassName.simpleName]
317+
return selectedFields == cachedTypeFields
318+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
query Operation1 {
2+
first: complexObjectQuery {
3+
id
4+
name
5+
}
6+
second: complexObjectQuery {
7+
id
8+
name
9+
details {
10+
id
11+
value
12+
}
13+
}
14+
third: complexObjectQuery {
15+
id
16+
name
17+
details {
18+
id
19+
}
20+
}
21+
fourth: complexObjectQuery {
22+
id
23+
name
24+
}
25+
fifth: complexObjectQuery {
26+
id
27+
name
28+
details {
29+
id
30+
value
31+
}
32+
}
33+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.expediagroup.graphql.generated
2+
3+
import com.expediagroup.graphql.client.Generated
4+
import com.expediagroup.graphql.client.types.GraphQLClientRequest
5+
import com.expediagroup.graphql.generated.responses.ComplexObject
6+
import com.expediagroup.graphql.generated.responses.ComplexObject2
7+
import com.expediagroup.graphql.generated.responses.ComplexObject3
8+
import com.fasterxml.jackson.`annotation`.JsonProperty
9+
import kotlin.String
10+
import kotlin.reflect.KClass
11+
12+
public const val OPERATION1: String =
13+
"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}"
14+
15+
@Generated
16+
public class Operation1 : GraphQLClientRequest<Operation1.Result> {
17+
override val query: String = OPERATION1
18+
19+
override val operationName: String = "Operation1"
20+
21+
override fun responseType(): KClass<Operation1.Result> = Operation1.Result::class
22+
23+
@Generated
24+
public data class Result(
25+
/**
26+
* Query returning an object that references another object
27+
*/
28+
@get:JsonProperty(value = "first")
29+
public val first: ComplexObject,
30+
/**
31+
* Query returning an object that references another object
32+
*/
33+
@get:JsonProperty(value = "second")
34+
public val second: ComplexObject2,
35+
/**
36+
* Query returning an object that references another object
37+
*/
38+
@get:JsonProperty(value = "third")
39+
public val third: ComplexObject3,
40+
/**
41+
* Query returning an object that references another object
42+
*/
43+
@get:JsonProperty(value = "fourth")
44+
public val fourth: ComplexObject,
45+
/**
46+
* Query returning an object that references another object
47+
*/
48+
@get:JsonProperty(value = "fifth")
49+
public val fifth: ComplexObject2,
50+
)
51+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
query Operation2 {
2+
first: complexObjectQuery {
3+
id
4+
name
5+
}
6+
second: complexObjectQuery {
7+
id
8+
name
9+
details {
10+
id
11+
value
12+
}
13+
}
14+
third: complexObjectQuery {
15+
id
16+
name
17+
details {
18+
id
19+
}
20+
}
21+
fourth: complexObjectQuery {
22+
id
23+
name
24+
}
25+
fifth: complexObjectQuery {
26+
id
27+
name
28+
details {
29+
id
30+
value
31+
}
32+
}
33+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.expediagroup.graphql.generated
2+
3+
import com.expediagroup.graphql.client.Generated
4+
import com.expediagroup.graphql.client.types.GraphQLClientRequest
5+
import com.expediagroup.graphql.generated.responses.ComplexObject
6+
import com.expediagroup.graphql.generated.responses.ComplexObject2
7+
import com.expediagroup.graphql.generated.responses.ComplexObject3
8+
import com.fasterxml.jackson.`annotation`.JsonProperty
9+
import kotlin.String
10+
import kotlin.reflect.KClass
11+
12+
public const val OPERATION2: String =
13+
"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}"
14+
15+
@Generated
16+
public class Operation2 : GraphQLClientRequest<Operation2.Result> {
17+
override val query: String = OPERATION2
18+
19+
override val operationName: String = "Operation2"
20+
21+
override fun responseType(): KClass<Operation2.Result> = Operation2.Result::class
22+
23+
@Generated
24+
public data class Result(
25+
/**
26+
* Query returning an object that references another object
27+
*/
28+
@get:JsonProperty(value = "first")
29+
public val first: ComplexObject,
30+
/**
31+
* Query returning an object that references another object
32+
*/
33+
@get:JsonProperty(value = "second")
34+
public val second: ComplexObject2,
35+
/**
36+
* Query returning an object that references another object
37+
*/
38+
@get:JsonProperty(value = "third")
39+
public val third: ComplexObject3,
40+
/**
41+
* Query returning an object that references another object
42+
*/
43+
@get:JsonProperty(value = "fourth")
44+
public val fourth: ComplexObject,
45+
/**
46+
* Query returning an object that references another object
47+
*/
48+
@get:JsonProperty(value = "fifth")
49+
public val fifth: ComplexObject2,
50+
)
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.expediagroup.graphql.generated.responses
2+
3+
import com.expediagroup.graphql.client.Generated
4+
import com.fasterxml.jackson.`annotation`.JsonProperty
5+
import kotlin.Int
6+
import kotlin.String
7+
8+
/**
9+
* Multi line description of a complex type.
10+
* This is a second line of the paragraph.
11+
* This is final line of the description.
12+
*/
13+
@Generated
14+
public data class ComplexObject(
15+
/**
16+
* Some unique identifier
17+
*/
18+
@get:JsonProperty(value = "id")
19+
public val id: Int,
20+
/**
21+
* Some object name
22+
*/
23+
@get:JsonProperty(value = "name")
24+
public val name: String,
25+
)

0 commit comments

Comments
 (0)