Skip to content

Commit 7d1edcc

Browse files
authored
Merge pull request #855 from graphql-java-kickstart/bugfix/552
Merge type extensions into the types they extend
2 parents 0140de6 + 2241889 commit 7d1edcc

6 files changed

Lines changed: 632 additions & 51 deletions

File tree

‎src/main/kotlin/graphql/kickstart/tools/ScannedSchemaObjects.kt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import graphql.schema.GraphQLScalarType
1616
internal data class ScannedSchemaObjects(
1717
val dictionary: TypeClassDictionary,
1818
val definitions: Set<SDLNamedDefinition<*>>,
19+
val typeExtensions: TypeExtensions,
1920
val customScalars: CustomScalarMap,
2021
val rootInfo: RootTypeInfo,
2122
val fieldResolversByType: Map<ObjectTypeDefinition, MutableMap<FieldDefinition, FieldResolver>>,
@@ -25,3 +26,5 @@ internal data class ScannedSchemaObjects(
2526

2627
internal typealias TypeClassDictionary = BiMap<TypeDefinition<*>, JavaType>
2728
internal typealias CustomScalarMap = Map<String, GraphQLScalarType>
29+
// the extensions of each type by type name, in the order they're declared
30+
internal typealias TypeExtensions = Map<String, List<TypeDefinition<*>>>

‎src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt‎

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,25 @@ internal class SchemaClassScanner(
3535
private val resolverInfos = resolvers.asSequence().minus(queryResolvers).minus(mutationResolvers).minus(subscriptionResolvers).map { NormalResolverInfo(it, options) }.toList()
3636

3737
private val initialDictionary = initialDictionary.mapValues { InitialDictionaryEntry(it.value) }
38-
private val extensionDefinitions = allDefinitions.filterIsInstance<ObjectTypeExtensionDefinition>()
39-
private val inputExtensionDefinitions = allDefinitions.filterIsInstance<InputObjectTypeExtensionDefinition>()
4038
private val directiveDefinitions = allDefinitions.filterIsInstance<DirectiveDefinition>()
4139
private val scalarDefinitions = allDefinitions.filterIsInstance<ScalarTypeDefinition>()
4240

43-
private val definitionsByName = (allDefinitions.filterIsInstance<TypeDefinition<*>>() - extensionDefinitions - inputExtensionDefinitions).associateBy { it.name }
44-
private val objectDefinitions = (allDefinitions.filterIsInstance<ObjectTypeDefinition>() - extensionDefinitions)
41+
private val typeDefinitions = allDefinitions.filterIsInstance<TypeDefinition<*>>()
42+
private val baseTypeNames = typeDefinitions.filter { it !is SDLExtensionDefinition }.map { it.name }.toSet()
43+
44+
// the first extension of an undefined union, enum, interface or scalar stands in for its definition
45+
private val loneExtensions = typeDefinitions
46+
.filter { it is SDLExtensionDefinition && it !is ObjectTypeDefinition && it !is InputObjectTypeDefinition && it.name !in baseTypeNames }
47+
.distinctBy { it.name }
48+
49+
private val definitionsByName = typeDefinitions.filter { it !is SDLExtensionDefinition || it in loneExtensions }.associateBy { it.name }
50+
// without extensions of a different kind than their type, which are ignored with a warning, see init
51+
private val typeExtensions: TypeExtensions = typeDefinitions
52+
.filter { it is SDLExtensionDefinition && it !in loneExtensions && !extendsOtherKind(it) }
53+
.groupBy { it.name }
54+
private val objectDefinitions = allDefinitions.filterIsInstance<ObjectTypeDefinition>().filter { it !is ObjectTypeExtensionDefinition }
4555
private val objectDefinitionsByName = objectDefinitions.associateBy { it.name }
46-
private val interfaceDefinitionsByName = allDefinitions.filterIsInstance<InterfaceTypeDefinition>().associateBy { it.name }
56+
private val interfaceDefinitionsByName = definitionsByName.values.filterIsInstance<InterfaceTypeDefinition>().associateBy { it.name }
4757

4858
private val fieldResolverScanner = FieldResolverScanner(options)
4959
private val typeClassMatcher = TypeClassMatcher(definitionsByName)
@@ -61,6 +71,11 @@ internal class SchemaClassScanner(
6171
}
6272
}
6373

74+
typeDefinitions.filter { extendsOtherKind(it) }.forEach { extension ->
75+
val definition = definitionsByName.getValue(extension.name)
76+
log.warn("Type '${extension.name}' is defined as ${definition.kind()}, but extended as ${extension.kind()}. The extension is ignored.")
77+
}
78+
6479
// Input object types never go into the type => class bimap that interface and union type resolution use, so several of
6580
// them can share a class, e.g. ObjectNode
6681
initialDictionary
@@ -216,9 +231,8 @@ internal class SchemaClassScanner(
216231
* The names of the types used by the fields, arguments and input fields of [definitions], including fields added by extensions.
217232
*/
218233
private fun typeNamesUsedByFields(definitions: Set<TypeDefinition<*>>): Set<String> {
219-
val names = definitions.map { it.name }.toSet()
220-
return (definitions + extensionDefinitions + inputExtensionDefinitions)
221-
.filter { it.name in names }
234+
return definitions
235+
.flatMap { it.withExtensions(typeExtensions) }
222236
.flatMap { definition ->
223237
when (definition) {
224238
is ImplementingTypeDefinition<*> -> definition.fieldDefinitions.flatMap { field ->
@@ -278,6 +292,7 @@ internal class SchemaClassScanner(
278292
.description(getDocumentation(definition, options) ?: provided.description)
279293
.coercing(provided.coercing)
280294
.definition(definition)
295+
.extensionDefinitions(typeExtensions.extensionsOf<ScalarTypeExtensionDefinition>(definition.name))
281296
.build()
282297
}.associateBy { it.name }
283298

@@ -300,15 +315,15 @@ internal class SchemaClassScanner(
300315
validateRootResolversWereUsed(rootTypeHolder.mutation, fieldResolvers)
301316
validateRootResolversWereUsed(rootTypeHolder.subscription, fieldResolvers)
302317

303-
val definitions = observedDefinitions + extensionDefinitions + inputExtensionDefinitions + directiveDefinitions
318+
val definitions = observedDefinitions + directiveDefinitions
304319

305320
// The enums in unvalidatedTypes come from directives without a class, so they can only be built when no field uses them.
306321
// A field hands the enum's values to user code even when the scanner found no class through it, e.g. an input field bound
307322
// to a Map or an unimplemented field's argument.
308323
val typeNamesUsedByFields = typeNamesUsedByFields(observedDefinitions)
309324
val directiveOnlyEnums = unvalidatedTypes.filterIsInstance<EnumTypeDefinition>().filter { it.name !in typeNamesUsedByFields }.toSet()
310325

311-
return ScannedSchemaObjects(dictionary, definitions, scalars, rootInfo, fieldResolversByType.toMap(), unusedDefinitions, directiveOnlyEnums)
326+
return ScannedSchemaObjects(dictionary, definitions, typeExtensions, scalars, rootInfo, fieldResolversByType.toMap(), unusedDefinitions, directiveOnlyEnums)
312327
}
313328

314329
private fun validateRootResolversWereUsed(rootType: RootType?, fieldResolvers: List<FieldResolver>) {
@@ -326,16 +341,20 @@ internal class SchemaClassScanner(
326341

327342
private fun getAllObjectTypesImplementingDiscoveredInterfaces(): List<ObjectTypeDefinition> {
328343
return dictionary.keys.filterIsInstance<InterfaceTypeDefinition>().map { iface ->
329-
objectDefinitions.filter { obj -> obj.implements.filterIsInstance<TypeName>().any { it.name == iface.name } }
344+
objectDefinitions.filter { obj ->
345+
obj.withExtensions(typeExtensions).flatMap { it.implements }.filterIsInstance<TypeName>().any { it.name == iface.name }
346+
}
330347
}.flatten().distinctBy { it.name }
331348
}
332349

333350
private fun getAllObjectTypeMembersOfDiscoveredUnions(): List<ObjectTypeDefinition> {
334351
val unionTypeNames = dictionary.keys.filterIsInstance<UnionTypeDefinition>().map { union -> union.name }.toSet()
335352
return dictionary.keys.filterIsInstance<UnionTypeDefinition>().map { union ->
336-
union.memberTypes.filterIsInstance<TypeName>().filter { !unionTypeNames.contains(it.name) }.map {
337-
objectDefinitionsByName[it.name]
338-
?: throw SchemaClassScannerError("No object type found with name '${it.name}' for union: $union")
353+
union.withExtensions(typeExtensions).flatMap { definition ->
354+
definition.memberTypes.filterIsInstance<TypeName>().filter { !unionTypeNames.contains(it.name) }.map {
355+
objectDefinitionsByName[it.name]
356+
?: throw SchemaClassScannerError("No object type found with name '${it.name}' for union: $definition")
357+
}
339358
}
340359
}.flatten().distinct()
341360
}
@@ -346,7 +365,7 @@ internal class SchemaClassScanner(
346365
*/
347366
private fun handleInterfaceFieldTypes(): Boolean {
348367
val newTypes = dictionary.keys.filterIsInstance<InterfaceTypeDefinition>().flatMap { iface ->
349-
iface.fieldDefinitions.mapNotNull { field ->
368+
iface.withExtensions(typeExtensions).flatMap { it.fieldDefinitions }.mapNotNull { field ->
350369
val type = definitionsByName[(field.type.unwrap() as TypeName).name]
351370
if ((type is UnionTypeDefinition || type is InterfaceTypeDefinition) && !dictionary.containsKey(type)) {
352371
type to FieldTypeReference("${iface.name}.${field.name}")
@@ -429,8 +448,8 @@ internal class SchemaClassScanner(
429448
dictionary
430449
.filterKeys { definition ->
431450
when (definition) {
432-
is UnionTypeDefinition -> definition.memberTypes.filterIsInstance<TypeName>().any { it.name == type.name }
433-
is InterfaceTypeDefinition -> type.implements.filterIsInstance<TypeName>().any { it.name == definition.name }
451+
is UnionTypeDefinition -> definition.withExtensions(typeExtensions).flatMap { it.memberTypes }.filterIsInstance<TypeName>().any { it.name == type.name }
452+
is InterfaceTypeDefinition -> type.withExtensions(typeExtensions).flatMap { it.implements }.filterIsInstance<TypeName>().any { it.name == definition.name }
434453
else -> false
435454
}
436455
}
@@ -503,7 +522,7 @@ internal class SchemaClassScanner(
503522
resolverInfo.dataClassType != Any::class.java && resolverInfo.dataClassType.isAssignableFrom(dataClass)
504523

505524
private fun scanResolverInfoForPotentialMatches(type: ObjectTypeDefinition, resolverInfo: ResolverInfo) {
506-
type.getExtendedFieldDefinitions(extensionDefinitions).forEach { field ->
525+
type.withExtensions(typeExtensions).flatMap { it.fieldDefinitions }.forEach { field ->
507526
val fieldResolver = fieldResolverScanner.findFieldResolver(field, resolverInfo)
508527

509528
fieldResolversByType.getOrPut(type) { mutableMapOf() }[fieldResolver.field] = fieldResolver
@@ -573,8 +592,7 @@ internal class SchemaClassScanner(
573592
*/
574593
private fun handleInputObjectFields(graphQLType: InputObjectTypeDefinition, javaType: JavaType?) {
575594
val clazz = javaType?.unwrap()
576-
val inputObjectTypes = listOf(graphQLType) + inputExtensionDefinitions.filter { it.name == graphQLType.name }
577-
inputObjectTypes
595+
graphQLType.withExtensions(typeExtensions)
578596
.flatMap { it.inputValueDefinitions }
579597
.forEach { inputValueDefinition ->
580598
val inputGraphQLType = inputValueDefinition.type.unwrap()
@@ -645,14 +663,30 @@ internal class SchemaClassScanner(
645663
}
646664

647665
private fun scanInterfacesOfType(graphQLType: ObjectTypeDefinition) {
648-
graphQLType.implements.forEach {
666+
graphQLType.withExtensions(typeExtensions).flatMap { it.implements }.forEach {
649667
if (it is TypeName) {
650668
handleFoundType(interfaceDefinitionsByName[it.name]
651669
?: throw SchemaClassScannerError("Object type ${graphQLType.name} declared interface ${it.name}, but no interface with that name was found in the schema!"), null, InterfaceReference(graphQLType))
652670
}
653671
}
654672
}
655673

674+
// e.g. `extend union Foo` of `type Foo`
675+
private fun extendsOtherKind(definition: TypeDefinition<*>): Boolean {
676+
val type = definitionsByName[definition.name]
677+
return definition is SDLExtensionDefinition && type != null && type.kind() != definition.kind()
678+
}
679+
680+
private fun TypeDefinition<*>.kind(): String = when (this) {
681+
is ObjectTypeDefinition -> "an object type"
682+
is InterfaceTypeDefinition -> "an interface"
683+
is UnionTypeDefinition -> "a union"
684+
is EnumTypeDefinition -> "an enum"
685+
is InputObjectTypeDefinition -> "an input object type"
686+
is ScalarTypeDefinition -> "a scalar"
687+
else -> javaClass.simpleName
688+
}
689+
656690
private fun enqueue(graphQLType: ObjectTypeDefinition, javaType: JavaType) {
657691
queue.add(QueueItem(graphQLType, javaType))
658692
}

0 commit comments

Comments
 (0)