Skip to content

Commit cf6c379

Browse files
Extend single-operation reuse logic to work across multiple operations
- Modify generateCustomClassName to use cross-operation reuse logic for shared types - Add isCachedTypeApplicableForSharedType function for selection set verification - Update selection set tracking to work with type variants across operations - Create cross_operation_reuse test case with Operation1 and Operation2 - Generate exactly 3 shared types (ComplexObject, ComplexObject2, ComplexObject3) - Maintain backward compatibility with existing reuse_types functionality - Fix missing ScalarTypeDefinition import that caused compilation errors - Verify functionality with gradle-client and maven-client examples Co-Authored-By: Arthur Poon <[email protected]>
1 parent 7292a06 commit cf6c379

File tree

5 files changed

+157
-28
lines changed

5 files changed

+157
-28
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ data class GraphQLClientGeneratorContext(
5656
val classNameCache: MutableMap<String, MutableList<ClassName>> = mutableMapOf()
5757
val typeToSelectionSetMap: MutableMap<String, Set<String>> = mutableMapOf()
5858
val responseTypeToSelectionSetMap: MutableMap<String, MutableSet<Selection<*>>> = mutableMapOf()
59+
val sharedTypeVariantToSelectionSetMap: MutableMap<String, Set<String>> = mutableMapOf()
5960

6061
// usage tracking for shared response types
6162
val typeUsageCount: MutableMap<String, Int> = mutableMapOf()

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

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import graphql.language.NamedNode
4242
import graphql.language.NonNullType
4343
import graphql.language.ObjectTypeDefinition
4444
import graphql.language.ScalarTypeDefinition
45-
import graphql.language.Selection
4645
import graphql.language.SelectionSet
4746
import graphql.language.Type
4847
import graphql.language.TypeDefinition
@@ -112,25 +111,49 @@ internal fun generateCustomClassName(context: GraphQLClientGeneratorContext, gra
112111
// generate corresponding type spec
113112
when (graphQLTypeDefinition) {
114113
is ObjectTypeDefinition -> {
115-
// Check if this should be a shared response type
116-
val sharedClassName = ClassName("${context.packageName}.responses", graphQLTypeDefinition.name)
117-
if (context.responseClassToTypeSpecs.containsKey(sharedClassName)) {
118-
// Update existing shared type with merged selection set
119-
val mergedSelectionSet = mergeSelectionSets(context, graphQLTypeDefinition.name, selectionSet)
120-
context.responseClassToTypeSpecs[sharedClassName] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, mergedSelectionSet)
121-
className = sharedClassName
122-
} else {
123-
// Check if this type should be shared (appears in multiple operations)
124-
if (shouldCreateSharedResponseType(context, graphQLTypeDefinition.name)) {
125-
// Create new shared response type
126-
val mergedSelectionSet = mergeSelectionSets(context, graphQLTypeDefinition.name, selectionSet)
127-
context.responseClassToTypeSpecs[sharedClassName] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, mergedSelectionSet)
128-
className = sharedClassName
114+
if (shouldCreateSharedResponseType(context, graphQLTypeDefinition.name)) {
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+
}
129142
} else {
130-
// Use original logic for operation-specific types
131-
className = generateClassName(context, graphQLTypeDefinition, selectionSet)
132-
context.typeSpecs[className] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet)
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+
}
133152
}
153+
} else {
154+
// Use original logic for operation-specific types
155+
className = generateClassName(context, graphQLTypeDefinition, selectionSet)
156+
context.typeSpecs[className] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet)
134157
}
135158
}
136159
is InputObjectTypeDefinition -> {
@@ -293,20 +316,31 @@ private fun shouldCreateSharedResponseType(context: GraphQLClientGeneratorContex
293316
return usageCount > 1
294317
}
295318

319+
/**
320+
* Helper function to check if a cached shared type matches the current selection set.
321+
*/
322+
private fun isCachedTypeApplicableForSharedType(
323+
context: GraphQLClientGeneratorContext,
324+
cachedClassName: ClassName,
325+
graphQLTypeDefinition: TypeDefinition<*>,
326+
selectionSet: SelectionSet?
327+
): Boolean {
328+
if (selectionSet == null) return true
329+
330+
val selectedFields = calculateSelectedFields(context, graphQLTypeDefinition.name, selectionSet)
331+
val cachedTypeFields = context.sharedTypeVariantToSelectionSetMap[cachedClassName.simpleName]
332+
return selectedFields == cachedTypeFields
333+
}
334+
296335
/**
297336
* Merges selection sets for the same GraphQL type across different operations.
298-
* This creates a comprehensive selection set that includes all fields selected in any operation.
337+
* For shared response types, we don't merge - we use exact selection sets for each variant.
338+
* This maintains the existing reuse_types behavior where different selection sets create different variants.
299339
*/
300340
private fun mergeSelectionSets(context: GraphQLClientGeneratorContext, typeName: String, currentSelectionSet: SelectionSet?): SelectionSet? {
301341
if (currentSelectionSet == null) return null
302342

303-
// Get existing selections for this type
304-
val existingSelections = context.responseTypeToSelectionSetMap.getOrPut(typeName) { mutableSetOf() }
305-
306-
// Add current selections
307-
existingSelections.addAll(currentSelectionSet.selections)
308-
309-
// Create merged selection set using the correct builder pattern
310-
val selectionsList: List<Selection<*>> = existingSelections.toList()
311-
return SelectionSet.newSelectionSet(selectionsList).build()
343+
// For shared response types, we don't merge - we use the exact selection set for each variant
344+
// This maintains the existing reuse_types behavior where different selection sets create different variants
345+
return currentSelectionSet
312346
}
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: 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+
}

plugins/client/graphql-kotlin-client-generator/src/test/kotlin/com/expediagroup/graphql/plugin/client/generator/SharedResponseTypesTest.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,32 @@ class SharedResponseTypesTest {
9595
)
9696
assertFalse(configExplicitlyDisabled.useSharedResponseTypes, "Expected useSharedResponseTypes to be false when explicitly disabled")
9797
}
98+
99+
@Test
100+
fun `verify cross-operation type reuse generates exactly 3 shared types`() {
101+
val configWithSharedTypes = GraphQLClientGeneratorConfig(
102+
packageName = "com.expediagroup.graphql.generated",
103+
useSharedResponseTypes = true
104+
)
105+
106+
val testDir = File("src/test/data/generator/cross_operation_reuse")
107+
val queries = testDir.walkTopDown()
108+
.filter { it.name.endsWith(".graphql") }
109+
.toList()
110+
111+
val generator = GraphQLClientGenerator(TEST_SCHEMA_PATH, configWithSharedTypes)
112+
val fileSpecs = generator.generate(queries)
113+
114+
assertTrue(fileSpecs.isNotEmpty())
115+
116+
// Check if exactly 3 shared response types are generated
117+
val sharedResponseTypes = fileSpecs.filter { it.packageName.endsWith(".responses") }
118+
val complexObjectTypes = sharedResponseTypes.filter { it.name.startsWith("ComplexObject") }
119+
120+
assertEquals(3, complexObjectTypes.size, "Expected exactly 3 ComplexObject variants")
121+
122+
// Verify the specific variants exist
123+
val typeNames = complexObjectTypes.map { it.name }.sorted()
124+
assertEquals(listOf("ComplexObject", "ComplexObject2", "ComplexObject3"), typeNames)
125+
}
98126
}

0 commit comments

Comments
 (0)