Skip to content

Commit bed8c8d

Browse files
oryan-blockclaude
andcommitted
Build scalars in SchemaParser so they get their directives
Scalars were rebuilt in SchemaClassScanner from the provided GraphQLScalarType with only its name, description and coercing, so directives from the SDL, @specifiedBy, the provided specifiedByUrl and onScalar wiring were all lost. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 21f3796 commit bed8c8d

5 files changed

Lines changed: 223 additions & 30 deletions

File tree

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

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import graphql.kickstart.tools.resolver.MethodFieldResolver
77
import graphql.kickstart.tools.resolver.PropertyFieldResolver
88
import graphql.kickstart.tools.util.*
99
import graphql.language.*
10-
import graphql.schema.GraphQLScalarType
1110
import graphql.schema.idl.ScalarInfo
1211
import graphql.schema.idl.TypeUtil
1312
import org.apache.commons.lang3.ClassUtils
@@ -282,24 +281,15 @@ internal class SchemaClassScanner(
282281
} catch (t: Throwable) {
283282
throw SchemaClassScannerError("Error creating bimap of type => class", t)
284283
}
285-
val scalarDefinitions = observedDefinitions.filterIsInstance<ScalarTypeDefinition>()
286-
287-
// Ensure all scalar definitions have implementations and add the definition to those.
288-
val scalars = scalarDefinitions
284+
// SchemaParser builds the scalars, this only checks that each one was provided
285+
val scalars = observedDefinitions.filterIsInstance<ScalarTypeDefinition>()
289286
.filter {
290287
// Filter for any defined scalars OR scalars that aren't defined but also aren't standard
291288
scalars.containsKey(it.name) || !ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(it.name)
292-
}.map { definition ->
293-
val provided = scalars[definition.name]
294-
?: throw SchemaClassScannerError("Expected a user-defined GraphQL scalar type with name '${definition.name}' but found none!")
295-
GraphQLScalarType.newScalar()
296-
.name(provided.name)
297-
.description(getDocumentation(definition, options) ?: provided.description)
298-
.coercing(provided.coercing)
299-
.definition(definition)
300-
.extensionDefinitions(typeExtensions.extensionsOf<ScalarTypeExtensionDefinition>(definition.name))
301-
.build()
302-
}.associateBy { it.name }
289+
}.associate { definition ->
290+
definition.name to (scalars[definition.name]
291+
?: throw SchemaClassScannerError("Expected a user-defined GraphQL scalar type with name '${definition.name}' but found none!"))
292+
}
303293

304294
val unusedDefinitions = (definitionsByName.values - observedDefinitions).toSet()
305295
unusedDefinitions

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

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import graphql.introspection.Introspection.DirectiveLocation.INPUT_FIELD_DEFINIT
1111
import graphql.introspection.Introspection.DirectiveLocation.INPUT_OBJECT
1212
import graphql.introspection.Introspection.DirectiveLocation.INTERFACE
1313
import graphql.introspection.Introspection.DirectiveLocation.OBJECT
14+
import graphql.introspection.Introspection.DirectiveLocation.SCALAR
1415
import graphql.introspection.Introspection.DirectiveLocation.SCHEMA
1516
import graphql.introspection.Introspection.DirectiveLocation.UNION
1617
import graphql.kickstart.tools.directive.DirectiveWiringHelper
@@ -60,6 +61,7 @@ class SchemaParser internal constructor(
6061
private val directiveDefinitions = definitions.filterIsInstance<DirectiveDefinition>()
6162

6263
private val unionDefinitions = definitions.filterIsInstance<UnionTypeDefinition>()
64+
private val scalarDefinitions = definitions.filterIsInstance<ScalarTypeDefinition>().filter { it.name in customScalars }.associateBy { it.name }
6365

6466
private val permittedTypesForObject: Set<String> = (objectDefinitions.map { it.name } +
6567
enumDefinitions.map { it.name } +
@@ -72,6 +74,7 @@ class SchemaParser internal constructor(
7274
private val directiveWiringHelper = DirectiveWiringHelper(runtimeWiring, codeRegistryBuilder)
7375

7476
private lateinit var schemaDirectives: SchemaDirectives
77+
private lateinit var scalars: MutableMap<String, GraphQLScalarType>
7578

7679
/**
7780
* Parses the given schema with respect to the given dictionary and returns GraphQL objects.
@@ -88,6 +91,7 @@ class SchemaParser internal constructor(
8891

8992
// Create GraphQL objects
9093
val inputObjects: MutableList<GraphQLInputObjectType> = mutableListOf()
94+
scalars = mutableMapOf()
9195
createDirectives(inputObjects)
9296
inputObjectDefinitions.forEach {
9397
if (inputObjects.none { io -> io.name == it.name }) {
@@ -262,6 +266,44 @@ class SchemaParser internal constructor(
262266
return directiveWiringHelper.wireEnum(builder.build())
263267
}
264268

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) }
280+
}
281+
282+
private fun createScalar(definition: ScalarTypeDefinition): GraphQLScalarType {
283+
val name = definition.name
284+
val provided = customScalars.getValue(name)
285+
val extensions = typeExtensions.extensionsOf<ScalarTypeExtensionDefinition>(name)
286+
val directives = directivesOf(definition, extensions)
287+
val (appliedDirectives, legacyDirectives) = try {
288+
buildAppliedDirectives(directives, SCALAR) to buildDirectives(directives, SCALAR)
289+
} catch (e: SchemaError) {
290+
// TODO throw instead in 15.0.0 (#893), like for the other types
291+
log.warn("Scalar '$name' is built without its directives from the schema: ${e.message}")
292+
emptyArray<GraphQLAppliedDirective>() to emptyArray<GraphQLDirective>()
293+
}
294+
val builder = GraphQLScalarType.newScalar(provided)
295+
.definition(definition)
296+
.extensionDefinitions(extensions)
297+
.description(getDocumentation(definition, options) ?: provided.description)
298+
.specifiedByUrl(getSpecifiedByUrl(directives) ?: provided.specifiedByUrl)
299+
// like graphql-java, only the schema's directives are applied
300+
.clearDirectives()
301+
.withAppliedDirectives(*appliedDirectives)
302+
.withDirectives(*legacyDirectives)
303+
304+
return directiveWiringHelper.wireScalar(builder.build())
305+
}
306+
265307
private fun createInterfaceObject(interfaceDefinition: InterfaceTypeDefinition, inputObjects: List<GraphQLInputObjectType>): GraphQLInterfaceType {
266308
val extensions = typeExtensions.extensionsOf<InterfaceTypeExtensionDefinition>(interfaceDefinition.name)
267309
val directives = directivesOf(interfaceDefinition, extensions)
@@ -391,13 +433,13 @@ class SchemaParser internal constructor(
391433

392434
/**
393435
* The directive definitions. The input objects used by their arguments need the directives applied to them, so each directive
394-
* is first built with type references to input objects, then rebuilt with the input objects the first time it's looked up.
395-
* Only a directive applied within its own argument types gets its first build there.
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.
396438
*/
397439
private inner class SchemaDirectives(private val inputObjects: MutableList<GraphQLInputObjectType>) {
398440
// by name, in the order they're declared
399441
private val byName = directiveDefinitions.associateTo(linkedMapOf()) { definition ->
400-
definition.name to createDirective(definition) { determineType(GraphQLInputType::class, it, permittedTypesForInputObject, inputObjects) as GraphQLInputType }
442+
definition.name to createDirective(definition, ::typeReferenceOf)
401443
}
402444
private val unfinished = directiveDefinitions.associateByTo(mutableMapOf()) { it.name }
403445
// shared, so that an input object that's still being built isn't built again for another directive
@@ -597,6 +639,20 @@ class SchemaParser internal constructor(
597639
private fun missingArgumentsWithDefault(directive: Directive, graphQLDirective: GraphQLDirective): List<GraphQLArgument> =
598640
graphQLDirective.arguments.filter { it.hasSetDefaultValue() && directive.getArgument(it.name) == null }
599641

642+
private fun getSpecifiedByUrl(directives: List<Directive>): String? =
643+
(directives.find { it.name == Directives.SpecifiedByDirective.name }?.getArgument("url")?.value as? StringValue)?.value
644+
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+
}
655+
600656
private fun determineOutputType(typeDefinition: Type<*>, inputObjects: List<GraphQLInputObjectType>) =
601657
determineType(GraphQLOutputType::class, typeDefinition, permittedTypesForObject, inputObjects) as GraphQLOutputType
602658

@@ -614,8 +670,7 @@ class SchemaParser internal constructor(
614670
createInputObject(typeDefinition, inputObjects, mutableSetOf())
615671
}
616672
is TypeName -> {
617-
val scalarType = customScalars[typeDefinition.name]
618-
?: GRAPHQL_SCALARS[typeDefinition.name]
673+
val scalarType = scalarType(typeDefinition.name)
619674
if (scalarType != null) {
620675
scalarType
621676
} else {
@@ -645,8 +700,7 @@ class SchemaParser internal constructor(
645700
createInputObject(typeDefinition, inputObjects, referencingInputObjects)
646701
}
647702
is TypeName -> {
648-
val scalarType = customScalars[typeDefinition.name]
649-
?: GRAPHQL_SCALARS[typeDefinition.name]
703+
val scalarType = scalarType(typeDefinition.name)
650704
if (scalarType != null) {
651705
scalarType
652706
} else {

‎src/main/kotlin/graphql/kickstart/tools/directive/DirectiveWiringHelper.kt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ internal class DirectiveWiringHelper(
4444
return wireDirectives(WiringWrapper(unionType, UNION, SchemaDirectiveWiring::onUnion))
4545
}
4646

47+
fun wireScalar(scalarType: GraphQLScalarType): GraphQLScalarType {
48+
return wireDirectives(WiringWrapper(scalarType, SCALAR, SchemaDirectiveWiring::onScalar))
49+
}
50+
4751
private fun wireFields(fieldsContainer: GraphQLFieldsContainer): List<GraphQLFieldDefinition> {
4852
return fieldsContainer.fields.map { field ->
4953
// wire arguments

0 commit comments

Comments
 (0)