Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import graphql.kickstart.tools.resolver.MethodFieldResolver
import graphql.kickstart.tools.resolver.PropertyFieldResolver
import graphql.kickstart.tools.util.*
import graphql.language.*
import graphql.schema.GraphQLScalarType
import graphql.schema.idl.ScalarInfo
import graphql.schema.idl.TypeUtil
import org.apache.commons.lang3.ClassUtils
Expand Down Expand Up @@ -282,24 +281,15 @@ internal class SchemaClassScanner(
} catch (t: Throwable) {
throw SchemaClassScannerError("Error creating bimap of type => class", t)
}
val scalarDefinitions = observedDefinitions.filterIsInstance<ScalarTypeDefinition>()

// Ensure all scalar definitions have implementations and add the definition to those.
val scalars = scalarDefinitions
// SchemaParser builds the scalars, this only checks that each one was provided
val scalars = observedDefinitions.filterIsInstance<ScalarTypeDefinition>()
.filter {
// Filter for any defined scalars OR scalars that aren't defined but also aren't standard
scalars.containsKey(it.name) || !ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(it.name)
}.map { definition ->
val provided = scalars[definition.name]
?: throw SchemaClassScannerError("Expected a user-defined GraphQL scalar type with name '${definition.name}' but found none!")
GraphQLScalarType.newScalar()
.name(provided.name)
.description(getDocumentation(definition, options) ?: provided.description)
.coercing(provided.coercing)
.definition(definition)
.extensionDefinitions(typeExtensions.extensionsOf<ScalarTypeExtensionDefinition>(definition.name))
.build()
}.associateBy { it.name }
}.associate { definition ->
definition.name to (scalars[definition.name]
?: throw SchemaClassScannerError("Expected a user-defined GraphQL scalar type with name '${definition.name}' but found none!"))
}

val unusedDefinitions = (definitionsByName.values - observedDefinitions).toSet()
unusedDefinitions
Expand Down
68 changes: 61 additions & 7 deletions src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import graphql.introspection.Introspection.DirectiveLocation.INPUT_OBJECT
import graphql.introspection.Introspection.DirectiveLocation.INTERFACE
import graphql.introspection.Introspection.DirectiveLocation.OBJECT
import graphql.introspection.Introspection.DirectiveLocation.SCALAR
import graphql.introspection.Introspection.DirectiveLocation.SCHEMA
import graphql.introspection.Introspection.DirectiveLocation.UNION
import graphql.kickstart.tools.directive.DirectiveWiringHelper
Expand Down Expand Up @@ -60,6 +61,7 @@
private val directiveDefinitions = definitions.filterIsInstance<DirectiveDefinition>()

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

private val permittedTypesForObject: Set<String> = (objectDefinitions.map { it.name } +
enumDefinitions.map { it.name } +
Expand All @@ -72,6 +74,7 @@
private val directiveWiringHelper = DirectiveWiringHelper(runtimeWiring, codeRegistryBuilder)

private lateinit var schemaDirectives: SchemaDirectives
private lateinit var scalars: MutableMap<String, GraphQLScalarType>

/**
* Parses the given schema with respect to the given dictionary and returns GraphQL objects.
Expand All @@ -88,6 +91,7 @@

// Create GraphQL objects
val inputObjects: MutableList<GraphQLInputObjectType> = mutableListOf()
scalars = mutableMapOf()
createDirectives(inputObjects)
inputObjectDefinitions.forEach {
if (inputObjects.none { io -> io.name == it.name }) {
Expand Down Expand Up @@ -262,6 +266,44 @@
return directiveWiringHelper.wireEnum(builder.build())
}

/**
* 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
* using it holds the same instance. The directives it applies are built first, so their definitions don't keep a type
* reference to it. The directives applied to scalars can still hold one to a scalar that's being built, which graphql-java
* replaces when it builds the schema.
*/
private fun scalarType(name: String?): GraphQLScalarType? {
val definition = scalarDefinitions[name] ?: return GRAPHQL_SCALARS[name]
scalars[definition.name]?.let { return it }
definition.withExtensions(typeExtensions).flatMap { it.directives }.forEach { schemaDirectives.find(it.name) }
return scalars.getOrPut(definition.name) { createScalar(definition) }
}

private fun createScalar(definition: ScalarTypeDefinition): GraphQLScalarType {
val name = definition.name
val provided = customScalars.getValue(name)
val extensions = typeExtensions.extensionsOf<ScalarTypeExtensionDefinition>(name)
val directives = directivesOf(definition, extensions)
val (appliedDirectives, legacyDirectives) = try {
buildAppliedDirectives(directives, SCALAR) to buildDirectives(directives, SCALAR)
} catch (e: SchemaError) {
// TODO throw instead in 15.0.0 (#893), like for the other types

Check warning on line 290 in src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this TODO comment.

See more on https://sonarcloud.io/project/issues?id=graphql-java-kickstart_graphql-java-tools&issues=AaFW1X0JDBg9rA7lvyS2&open=AaFW1X0JDBg9rA7lvyS2&pullRequest=894
log.warn("Scalar '$name' is built without its directives from the schema: ${e.message}")
emptyArray<GraphQLAppliedDirective>() to emptyArray<GraphQLDirective>()
}
val builder = GraphQLScalarType.newScalar(provided)
.definition(definition)
.extensionDefinitions(extensions)
.description(getDocumentation(definition, options) ?: provided.description)
.specifiedByUrl(getSpecifiedByUrl(directives) ?: provided.specifiedByUrl)
// like graphql-java, only the schema's directives are applied
.clearDirectives()
.withAppliedDirectives(*appliedDirectives)
.withDirectives(*legacyDirectives)

return directiveWiringHelper.wireScalar(builder.build())
}

private fun createInterfaceObject(interfaceDefinition: InterfaceTypeDefinition, inputObjects: List<GraphQLInputObjectType>): GraphQLInterfaceType {
val extensions = typeExtensions.extensionsOf<InterfaceTypeExtensionDefinition>(interfaceDefinition.name)
val directives = directivesOf(interfaceDefinition, extensions)
Expand Down Expand Up @@ -391,13 +433,13 @@

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

private fun getSpecifiedByUrl(directives: List<Directive>): String? =
(directives.find { it.name == Directives.SpecifiedByDirective.name }?.getArgument("url")?.value as? StringValue)?.value

// a directive's first build, see SchemaDirectives: no directive exists yet, so only scalars that apply none can be built
private fun typeReferenceOf(type: Type<*>): GraphQLInputType = when (type) {
is ListType -> GraphQLList(typeReferenceOf(type.type))
is NonNullType -> GraphQLNonNull(typeReferenceOf(type.type))
is TypeName -> {
val appliesDirectives = scalarDefinitions[type.name]?.withExtensions(typeExtensions)?.any { it.directives.isNotEmpty() } == true
(if (appliesDirectives) null else scalarType(type.name)) ?: GraphQLTypeReference(type.name)
}
else -> throw SchemaError("Unknown type: $type")
}

private fun determineOutputType(typeDefinition: Type<*>, inputObjects: List<GraphQLInputObjectType>) =
determineType(GraphQLOutputType::class, typeDefinition, permittedTypesForObject, inputObjects) as GraphQLOutputType

Expand All @@ -614,8 +670,7 @@
createInputObject(typeDefinition, inputObjects, mutableSetOf())
}
is TypeName -> {
val scalarType = customScalars[typeDefinition.name]
?: GRAPHQL_SCALARS[typeDefinition.name]
val scalarType = scalarType(typeDefinition.name)
if (scalarType != null) {
scalarType
} else {
Expand Down Expand Up @@ -645,8 +700,7 @@
createInputObject(typeDefinition, inputObjects, referencingInputObjects)
}
is TypeName -> {
val scalarType = customScalars[typeDefinition.name]
?: GRAPHQL_SCALARS[typeDefinition.name]
val scalarType = scalarType(typeDefinition.name)
if (scalarType != null) {
scalarType
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ internal class DirectiveWiringHelper(
return wireDirectives(WiringWrapper(unionType, UNION, SchemaDirectiveWiring::onUnion))
}

fun wireScalar(scalarType: GraphQLScalarType): GraphQLScalarType {
return wireDirectives(WiringWrapper(scalarType, SCALAR, SchemaDirectiveWiring::onScalar))
}

private fun wireFields(fieldsContainer: GraphQLFieldsContainer): List<GraphQLFieldDefinition> {
return fieldsContainer.fields.map { field ->
// wire arguments
Expand Down
Loading
Loading