Skip to content

Commit cb1deee

Browse files
oryan-blockclaude
andcommitted
Merge master into bugfix/864
Wirings run on the assembled schema since #895, so custom scalars are built once, put in the dictionary and referenced by name like the other types, and they're wired with the other types. That replaces building them on first use and the directive cycle handling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 parents bed8c8d + d47bac5 commit cb1deee

5 files changed

Lines changed: 331 additions & 142 deletions

File tree

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

Lines changed: 58 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ class SchemaParser internal constructor(
7373
private val codeRegistryBuilder = GraphQLCodeRegistry.newCodeRegistry()
7474
private val directiveWiringHelper = DirectiveWiringHelper(runtimeWiring, codeRegistryBuilder)
7575

76-
private lateinit var schemaDirectives: SchemaDirectives
77-
private lateinit var scalars: MutableMap<String, GraphQLScalarType>
76+
// by name, in the order they're declared
77+
private lateinit var schemaDirectives: MutableMap<String, GraphQLDirective>
7878

7979
/**
8080
* Parses the given schema with respect to the given dictionary and returns GraphQL objects.
@@ -91,22 +91,15 @@ class SchemaParser internal constructor(
9191

9292
// Create GraphQL objects
9393
val inputObjects: MutableList<GraphQLInputObjectType> = mutableListOf()
94-
scalars = mutableMapOf()
95-
createDirectives(inputObjects)
94+
createDirectives()
95+
val scalars = scalarDefinitions.values.map { createScalar(it) }
9696
inputObjectDefinitions.forEach {
9797
if (inputObjects.none { io -> io.name == it.name }) {
9898
inputObjects.add(createInputObject(it, inputObjects, mutableSetOf()))
9999
}
100100
}
101101
val interfaces = interfaceDefinitions.map { createInterfaceObject(it, inputObjects) }
102102
val objects = objectDefinitions.map { createObject(it, interfaces, inputObjects) }
103-
if (options.unmappedInputFields != UnmappedInputFields.IGNORE) {
104-
val checker = UnmappedInputFieldChecker(inputObjects.associateBy { it.name }, options.unmappedInputFields)
105-
objectDefinitions.forEach { type ->
106-
fieldResolversByType[type].orEmpty().values.filterIsInstance<MethodFieldResolver>().forEach { checker.check(type.name, it) }
107-
}
108-
checker.report()
109-
}
110103
val unions = unionDefinitions.map { createUnionObject(it, objects) }
111104
val enums = enumDefinitions.map { createEnumObject(it) }
112105

@@ -128,11 +121,37 @@ class SchemaParser internal constructor(
128121

129122
val additionalObjects = objects.filter { o -> o != query && o != subscription && o != mutation }
130123

131-
val types = (additionalObjects.toSet() as Set<GraphQLType>) + inputObjects + enums + interfaces + unions
124+
val types = (additionalObjects.toSet() as Set<GraphQLType>) + inputObjects + enums + interfaces + unions + scalars
132125
// Directives applied to the schema itself, e.g. `extend schema @link(...)`. Only GraphQLAppliedDirectives are built here, not
133126
// the deprecated GraphQLDirective ones: graphql-java doesn't resolve type references in those, so enum arguments would fail.
134127
val schemaAppliedDirectives = buildAppliedDirectives(rootInfo.getDirectives(), SCHEMA).toList()
135-
return SchemaObjects(query, mutation, subscription, types, schemaDirectives.all, codeRegistryBuilder, rootInfo.getDescription(), schemaAppliedDirectives)
128+
val schemaObjects = SchemaObjects(query, mutation, subscription, types, schemaDirectives.values.toSet(), codeRegistryBuilder, rootInfo.getDescription(), schemaAppliedDirectives)
129+
// in the order the types were created
130+
val wiredObjects = if (directiveWiringHelper.isWiringRequired) wire(schemaObjects, scalars + inputObjects + interfaces + objects + unions + enums) else schemaObjects
131+
if (options.unmappedInputFields != UnmappedInputFields.IGNORE) {
132+
val wiredInputObjects = wiredObjects.dictionary.filterIsInstance<GraphQLInputObjectType>().associateBy { it.name }
133+
val checker = UnmappedInputFieldChecker(wiredInputObjects, options.unmappedInputFields)
134+
objectDefinitions.forEach { type ->
135+
fieldResolversByType[type].orEmpty().values.filterIsInstance<MethodFieldResolver>().forEach { checker.check(type.name, it) }
136+
}
137+
checker.report()
138+
}
139+
return wiredObjects
140+
}
141+
142+
/**
143+
* Runs the directive wirings on the types once the schema is assembled, so they see resolved types, and swaps in the types they change.
144+
*/
145+
private fun wire(schemaObjects: SchemaObjects, types: List<GraphQLNamedType>): SchemaObjects {
146+
// building the schema resolves the type references in these types
147+
val wired = directiveWiringHelper.wireTypes(schemaObjects.toSchema(), types)
148+
if (wired.isEmpty()) return schemaObjects
149+
return schemaObjects.copy(
150+
query = wired[schemaObjects.query.name] as GraphQLObjectType? ?: schemaObjects.query,
151+
mutation = schemaObjects.mutation?.let { wired[it.name] as GraphQLObjectType? ?: it },
152+
subscription = schemaObjects.subscription?.let { wired[it.name] as GraphQLObjectType? ?: it },
153+
dictionary = schemaObjects.dictionary.mapTo(linkedSetOf()) { wired[(it as GraphQLNamedType).name] ?: it }
154+
)
136155
}
137156

138157
/**
@@ -182,7 +201,7 @@ class SchemaParser internal constructor(
182201
}
183202
}
184203

185-
return directiveWiringHelper.wireObject(builder.build())
204+
return builder.build()
186205
}
187206

188207
private fun createInputObject(definition: InputObjectTypeDefinition, inputObjects: List<GraphQLInputObjectType>,
@@ -218,7 +237,7 @@ class SchemaParser internal constructor(
218237
}
219238
}
220239

221-
return directiveWiringHelper.wireInputObject(builder.build())
240+
return builder.build()
222241
}
223242

224243
private fun createEnumObject(definition: EnumTypeDefinition): GraphQLEnumType {
@@ -263,20 +282,7 @@ class SchemaParser internal constructor(
263282
}
264283
}
265284

266-
return directiveWiringHelper.wireEnum(builder.build())
267-
}
268-
269-
/**
270-
* The scalar with this name, or null if there's none. A custom scalar is built the first time it's used, so that everything
271-
* using it holds the same instance. The directives it applies are built first, so their definitions don't keep a type
272-
* reference to it. The directives applied to scalars can still hold one to a scalar that's being built, which graphql-java
273-
* replaces when it builds the schema.
274-
*/
275-
private fun scalarType(name: String?): GraphQLScalarType? {
276-
val definition = scalarDefinitions[name] ?: return GRAPHQL_SCALARS[name]
277-
scalars[definition.name]?.let { return it }
278-
definition.withExtensions(typeExtensions).flatMap { it.directives }.forEach { schemaDirectives.find(it.name) }
279-
return scalars.getOrPut(definition.name) { createScalar(definition) }
285+
return builder.build()
280286
}
281287

282288
private fun createScalar(definition: ScalarTypeDefinition): GraphQLScalarType {
@@ -301,7 +307,7 @@ class SchemaParser internal constructor(
301307
.withAppliedDirectives(*appliedDirectives)
302308
.withDirectives(*legacyDirectives)
303309

304-
return directiveWiringHelper.wireScalar(builder.build())
310+
return builder.build()
305311
}
306312

307313
private fun createInterfaceObject(interfaceDefinition: InterfaceTypeDefinition, inputObjects: List<GraphQLInputObjectType>): GraphQLInterfaceType {
@@ -328,7 +334,7 @@ class SchemaParser internal constructor(
328334
}
329335
}
330336

331-
return directiveWiringHelper.wireInterFace(builder.build())
337+
return builder.build()
332338
}
333339

334340
private fun createUnionObject(definition: UnionTypeDefinition, types: List<GraphQLObjectType>): GraphQLUnionType {
@@ -346,7 +352,7 @@ class SchemaParser internal constructor(
346352
getLeafUnionObjects(definition, types).forEach { possibleType(it) }
347353
}
348354

349-
return directiveWiringHelper.wireUnion(builder.build())
355+
return builder.build()
350356
}
351357

352358
private fun getLeafUnionObjects(definition: UnionTypeDefinition, types: List<GraphQLObjectType>): List<GraphQLObjectType> {
@@ -395,7 +401,7 @@ class SchemaParser internal constructor(
395401
.description(getDocumentation(fieldDefinition, options))
396402
.definition(fieldDefinition)
397403
.apply { getDeprecated(fieldDefinition.directives)?.let { deprecate(it) } }
398-
.type(determineOutputType(fieldDefinition.type, inputObjects))
404+
.type(determineOutputType(fieldDefinition.type))
399405
.withAppliedDirectives(*buildAppliedDirectives(fieldDefinition.directives, FIELD_DEFINITION))
400406
.withDirectives(*buildDirectives(fieldDefinition.directives, FIELD_DEFINITION))
401407
.apply {
@@ -418,11 +424,10 @@ class SchemaParser internal constructor(
418424
.build()
419425
}
420426

421-
private fun createDirectives(inputObjects: MutableList<GraphQLInputObjectType>) {
422-
schemaDirectives = SchemaDirectives(inputObjects)
423-
directiveDefinitions.forEach { schemaDirectives.find(it.name) }
427+
private fun createDirectives() {
428+
schemaDirectives = directiveDefinitions.associateTo(linkedMapOf()) { it.name to createDirective(it) }
424429
// because the arguments can have directives too, we attach them only after the directives themselves are created
425-
schemaDirectives.update { d ->
430+
schemaDirectives.replaceAll { _, d ->
426431
val arguments = d.arguments.map { a -> a.transform {
427432
it.withAppliedDirectives(*buildAppliedDirectives(a.definition!!.directives, ARGUMENT_DEFINITION))
428433
.withDirectives(*buildDirectives(a.definition!!.directives, ARGUMENT_DEFINITION))
@@ -431,33 +436,7 @@ class SchemaParser internal constructor(
431436
}
432437
}
433438

434-
/**
435-
* The directive definitions. The input objects used by their arguments need the directives applied to them, so each directive
436-
* is first built with type references to the types it can't build yet, then rebuilt with the built types the first time it's
437-
* looked up. Only a directive applied within its own argument types gets its first build there.
438-
*/
439-
private inner class SchemaDirectives(private val inputObjects: MutableList<GraphQLInputObjectType>) {
440-
// by name, in the order they're declared
441-
private val byName = directiveDefinitions.associateTo(linkedMapOf()) { definition ->
442-
definition.name to createDirective(definition, ::typeReferenceOf)
443-
}
444-
private val unfinished = directiveDefinitions.associateByTo(mutableMapOf()) { it.name }
445-
// shared, so that an input object that's still being built isn't built again for another directive
446-
private val referencingInputObjects = mutableSetOf<String>()
447-
448-
val all: Set<GraphQLDirective> get() = byName.values.toSet()
449-
450-
fun find(name: String): GraphQLDirective? {
451-
unfinished.remove(name)?.let { definition ->
452-
byName[name] = createDirective(definition) { determineInputType(it, inputObjects, referencingInputObjects) }
453-
}
454-
return byName[name]
455-
}
456-
457-
fun update(transform: (GraphQLDirective) -> GraphQLDirective) = byName.replaceAll { _, directive -> transform(directive) }
458-
}
459-
460-
private fun createDirective(definition: DirectiveDefinition, determineArgumentType: (Type<*>) -> GraphQLInputType): GraphQLDirective {
439+
private fun createDirective(definition: DirectiveDefinition): GraphQLDirective {
461440
val locations = definition.directiveLocations.map { Introspection.DirectiveLocation.valueOf(it.name) }.toTypedArray()
462441

463442
return GraphQLDirective.newDirective()
@@ -469,18 +448,19 @@ class SchemaParser internal constructor(
469448
.repeatable(definition.isRepeatable)
470449
.apply {
471450
definition.inputValueDefinitions.forEach { argumentDefinition ->
472-
argument(createDirectiveArgument(argumentDefinition, determineArgumentType))
451+
argument(createDirectiveArgument(argumentDefinition))
473452
}
474453
}
475454
.build()
476455
}
477456

478-
private fun createDirectiveArgument(definition: InputValueDefinition, determineArgumentType: (Type<*>) -> GraphQLInputType): GraphQLArgument {
457+
private fun createDirectiveArgument(definition: InputValueDefinition): GraphQLArgument {
479458
return GraphQLArgument.newArgument()
480459
.name(definition.name)
481460
.definition(definition)
482461
.description(getDocumentation(definition, options))
483-
.type(determineArgumentType(definition.type))
462+
// type references, so that an input object used by a directive applied to it doesn't contain itself
463+
.type(determineType(GraphQLInputType::class, definition.type, permittedTypesForInputObject) as GraphQLInputType)
484464
.apply { getDeprecated(definition.directives)?.let { deprecate(it) } }
485465
.apply { definition.defaultValue?.let { defaultValueLiteral(it) } }
486466
.build()
@@ -550,7 +530,7 @@ class SchemaParser internal constructor(
550530
}
551531
}
552532

553-
private fun findDirective(name: String): GraphQLDirective? = schemaDirectives.find(name) ?: BUILT_IN_DIRECTIVES[name]
533+
private fun findDirective(name: String): GraphQLDirective? = schemaDirectives[name] ?: BUILT_IN_DIRECTIVES[name]
554534

555535
private fun buildUndeclaredAppliedDirective(directive: Directive): GraphQLAppliedDirective {
556536
return GraphQLAppliedDirective.newDirective()
@@ -642,33 +622,22 @@ class SchemaParser internal constructor(
642622
private fun getSpecifiedByUrl(directives: List<Directive>): String? =
643623
(directives.find { it.name == Directives.SpecifiedByDirective.name }?.getArgument("url")?.value as? StringValue)?.value
644624

645-
// a directive's first build, see SchemaDirectives: no directive exists yet, so only scalars that apply none can be built
646-
private fun typeReferenceOf(type: Type<*>): GraphQLInputType = when (type) {
647-
is ListType -> GraphQLList(typeReferenceOf(type.type))
648-
is NonNullType -> GraphQLNonNull(typeReferenceOf(type.type))
649-
is TypeName -> {
650-
val appliesDirectives = scalarDefinitions[type.name]?.withExtensions(typeExtensions)?.any { it.directives.isNotEmpty() } == true
651-
(if (appliesDirectives) null else scalarType(type.name)) ?: GraphQLTypeReference(type.name)
652-
}
653-
else -> throw SchemaError("Unknown type: $type")
654-
}
625+
// custom scalars are in the dictionary, so they're referenced by name like the other types
626+
private fun scalarType(name: String?): GraphQLInputType? =
627+
scalarDefinitions[name]?.let { GraphQLTypeReference(it.name) } ?: GRAPHQL_SCALARS[name]
655628

656-
private fun determineOutputType(typeDefinition: Type<*>, inputObjects: List<GraphQLInputObjectType>) =
657-
determineType(GraphQLOutputType::class, typeDefinition, permittedTypesForObject, inputObjects) as GraphQLOutputType
629+
private fun determineOutputType(typeDefinition: Type<*>) =
630+
determineType(GraphQLOutputType::class, typeDefinition, permittedTypesForObject) as GraphQLOutputType
658631

632+
// named types other than spec scalars are type references
659633
private fun <T : Any> determineType(
660634
expectedType: KClass<T>,
661635
typeDefinition: Type<*>,
662-
allowedTypeReferences: Set<String>,
663-
inputObjects: List<GraphQLInputObjectType>
636+
allowedTypeReferences: Set<String>
664637
): GraphQLType =
665638
when (typeDefinition) {
666-
is ListType -> GraphQLList(determineType(expectedType, typeDefinition.type, allowedTypeReferences, inputObjects))
667-
is NonNullType -> GraphQLNonNull(determineType(expectedType, typeDefinition.type, allowedTypeReferences, inputObjects))
668-
is InputObjectTypeDefinition -> {
669-
log.info("Create input object")
670-
createInputObject(typeDefinition, inputObjects, mutableSetOf())
671-
}
639+
is ListType -> GraphQLList(determineType(expectedType, typeDefinition.type, allowedTypeReferences))
640+
is NonNullType -> GraphQLNonNull(determineType(expectedType, typeDefinition.type, allowedTypeReferences))
672641
is TypeName -> {
673642
val scalarType = scalarType(typeDefinition.name)
674643
if (scalarType != null) {
@@ -677,7 +646,7 @@ class SchemaParser internal constructor(
677646
if (!allowedTypeReferences.contains(typeDefinition.name)) {
678647
throw unexpectedTypeError(typeDefinition, expectedType)
679648
}
680-
inputObjects.find { it.name == typeDefinition.name } ?: GraphQLTypeReference(typeDefinition.name)
649+
GraphQLTypeReference(typeDefinition.name)
681650
}
682651
}
683652
else -> throw SchemaError("Unknown type: $typeDefinition")

0 commit comments

Comments
 (0)