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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package graphql.kickstart.tools
import graphql.kickstart.tools.resolver.FieldResolver
import graphql.kickstart.tools.util.BiMap
import graphql.kickstart.tools.util.JavaType
import graphql.language.EnumTypeDefinition
import graphql.language.FieldDefinition
import graphql.language.ObjectTypeDefinition
import graphql.language.SDLNamedDefinition
Expand All @@ -18,7 +19,8 @@ internal data class ScannedSchemaObjects(
val customScalars: CustomScalarMap,
val rootInfo: RootTypeInfo,
val fieldResolversByType: Map<ObjectTypeDefinition, MutableMap<FieldDefinition, FieldResolver>>,
val unusedDefinitions: Set<TypeDefinition<*>>
val unusedDefinitions: Set<TypeDefinition<*>>,
val directiveOnlyEnums: Set<EnumTypeDefinition>
)

internal typealias TypeClassDictionary = BiMap<TypeDefinition<*>, JavaType>
Expand Down
54 changes: 50 additions & 4 deletions src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ internal class SchemaClassScanner(
private val typeClassMatcher = TypeClassMatcher(definitionsByName)
private val dictionary = mutableMapOf<TypeDefinition<*>, DictionaryEntry>()
private val unvalidatedTypes = mutableSetOf<TypeDefinition<*>>()
// enums that had no class when the directives using them were scanned, they're in unvalidatedTypes too
private val directiveEnumsWithoutClass = mutableSetOf<EnumTypeDefinition>()
private val queue = linkedSetOf<QueueItem>()

private val fieldResolversByType = mutableMapOf<ObjectTypeDefinition, MutableMap<FieldDefinition, FieldResolver>>()
Expand Down Expand Up @@ -171,15 +173,52 @@ internal class SchemaClassScanner(
?: error("No ${TypeDefinition::class.java.simpleName} for type name $inputTypeName")
when (typeDefinition) {
is ScalarTypeDefinition -> handleFoundScalarType(typeDefinition)
is EnumTypeDefinition -> handleDictionaryTypes(listOf(typeDefinition)) {
"Enum type '${it.name}' is used in a directive, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary."
}
is EnumTypeDefinition -> handleDirectiveEnum(typeDefinition)
is InputObjectTypeDefinition -> handleDictionaryTypes(listOf(typeDefinition)) {
"Input object type '${it.name}' is used in a directive, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary."
}
}
}

/**
* Directive argument values are never passed to resolver methods, so an enum that only directives use doesn't need a class.
* If no field, argument or input field uses it either, it's built with its value names as values, like graphql-java does,
* see [validateAndCreateResult].
*/
private fun handleDirectiveEnum(type: EnumTypeDefinition) {
if (dictionary.containsKey(type) || unvalidatedTypes.contains(type)) {
return
}

val initialEntry = initialDictionary[type.name]
if (initialEntry != null) {
handleFoundType(type, initialEntry.get(), DictionaryReference())
} else {
unvalidatedTypes.add(type)
directiveEnumsWithoutClass.add(type)
}
}

/**
* The names of the types that the fields, arguments and input fields of [definitions] use, including those added by extensions.
*/
private fun typeNamesUsedByFields(definitions: Set<TypeDefinition<*>>): Set<String> {
val names = definitions.map { it.name }.toSet()
return (definitions + extensionDefinitions + inputExtensionDefinitions)
.filter { it.name in names }
.flatMap { definition ->
when (definition) {
is ImplementingTypeDefinition<*> -> definition.fieldDefinitions.flatMap { field ->
listOf(field.type) + field.inputValueDefinitions.map { it.type }
}
is InputObjectTypeDefinition -> definition.inputValueDefinitions.map { it.type }
else -> emptyList()
}
}
.mapNotNull { (it.unwrap() as TypeName).name }
.toSet()
}

private fun validateAndCreateResult(rootTypeHolder: RootTypesHolder): ScannedSchemaObjects {
initialDictionary
.filter { !it.value.accessed }
Expand Down Expand Up @@ -246,7 +285,14 @@ internal class SchemaClassScanner(

val definitions = observedDefinitions + extensionDefinitions + inputExtensionDefinitions + directiveDefinitions

return ScannedSchemaObjects(dictionary, definitions, scalars, rootInfo, fieldResolversByType.toMap(), unusedDefinitions)
// Only enums that no field, argument or input field uses can do without a class. Any such use needs one, even where the
// scanner has no parameter or property to find it through, since the values then reach user code, e.g. through a
// Map-bound input or an unimplemented field's argument. Types found after the directives were scanned, e.g. unused types,
// count too: a class found through them is used, and an enum matched to Object or a Map is rejected by the parser.
val typeNamesUsedByFields = typeNamesUsedByFields(observedDefinitions)
val directiveOnlyEnums = directiveEnumsWithoutClass.filter { it.name !in typeNamesUsedByFields }.toSet()

return ScannedSchemaObjects(dictionary, definitions, scalars, rootInfo, fieldResolversByType.toMap(), unusedDefinitions, directiveOnlyEnums)
}

private fun validateRootResolversWereUsed(rootType: RootType?, fieldResolvers: List<FieldResolver>) {
Expand Down
14 changes: 10 additions & 4 deletions src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class SchemaParser internal constructor(
private val rootInfo = scanResult.rootInfo
private val fieldResolversByType = scanResult.fieldResolversByType
private val unusedDefinitions = scanResult.unusedDefinitions
private val directiveOnlyEnums = scanResult.directiveOnlyEnums

private val extensionDefinitions = definitions.filterIsInstance<ObjectTypeExtensionDefinition>()
private val inputExtensionDefinitions = definitions.filterIsInstance<InputObjectTypeExtensionDefinition>()
Expand Down Expand Up @@ -195,9 +196,10 @@ class SchemaParser internal constructor(

private fun createEnumObject(definition: EnumTypeDefinition): GraphQLEnumType {
val name = definition.name
// enums only used by directives can do without a class, see SchemaClassScanner.handleDirectiveEnum
val type = dictionary[definition]
?: throw SchemaError("Expected enum with name '$name' but found none!")
if (!type.unwrap().isEnum) throw SchemaError("Type '$name' is declared as an enum in the GraphQL schema but is not a Java enum!")
if (type == null && definition !in directiveOnlyEnums) throw SchemaError("Expected enum with name '$name' but found none!")
if (type != null && !type.unwrap().isEnum) throw SchemaError("Type '$name' is declared as an enum in the GraphQL schema but is not a Java enum!")

val builder = GraphQLEnumType.newEnum()
.name(name)
Expand All @@ -208,8 +210,12 @@ class SchemaParser internal constructor(
.apply {
definition.enumValueDefinitions.forEach { valueDefinition ->
val enumName = valueDefinition.name
val enumValue = type.unwrap().enumConstants.find { (it as Enum<*>).name == enumName }
?: throw SchemaError("Expected value for name '$enumName' in enum '${type.unwrap().simpleName}' but found none!")
val enumValue = if (type == null) {
enumName
} else {
type.unwrap().enumConstants.find { (it as Enum<*>).name == enumName }
?: throw SchemaError("Expected value for name '$enumName' in enum '${type.unwrap().simpleName}' but found none!")
}

value(
GraphQLEnumValueDefinition.newEnumValueDefinition()
Expand Down
Loading
Loading