diff --git a/src/main/kotlin/graphql/kickstart/tools/ScanDictionary.kt b/src/main/kotlin/graphql/kickstart/tools/ScanDictionary.kt new file mode 100644 index 00000000..187667ec --- /dev/null +++ b/src/main/kotlin/graphql/kickstart/tools/ScanDictionary.kt @@ -0,0 +1,269 @@ +package graphql.kickstart.tools + +import com.fasterxml.jackson.databind.JsonNode +import graphql.kickstart.tools.util.JavaType +import graphql.kickstart.tools.util.unwrap +import graphql.kickstart.tools.util.withExtensions +import graphql.language.* +import graphql.schema.idl.ScalarInfo +import org.slf4j.Logger +import java.lang.reflect.Member +import java.lang.reflect.Method + +internal class ScanDictionary( + private val options: SchemaParserOptions, + private val log: Logger, + private val typeExtensions: TypeExtensions, + private val definitionsByName: Map>, + private val typeClassMatcher: TypeClassMatcher, + private val initialDictionary: Map, + private val enqueue: (ObjectTypeDefinition, JavaType) -> Unit, + private val handleFoundScalarType: (ScalarTypeDefinition) -> Unit, + private val dictionary: MutableMap, DictionaryEntry> = mutableMapOf(), +) : Map, ScanDictionary.EntryView> by dictionary { + private val interfaceDefinitionsByName = definitionsByName.values.filterIsInstance().associateBy { it.name } + private val enumsWithoutClass = mutableMapOf() + + fun handleFoundType(match: TypeClassMatcher.Match) = when (match) { + is TypeClassMatcher.ScalarMatch -> handleFoundScalarType(match.type) + is TypeClassMatcher.ValidMatch -> handleFoundType(match.type, match.javaType, match.reference) + } + + /** + * Enter a found type into the dictionary if it doesn't exist yet, add a reference pointing back to where it was discovered. + */ + fun handleFoundType(type: TypeDefinition<*>, javaType: JavaType?, reference: Reference) { + val realEntry = dictionary.getOrPut(type) { DictionaryEntry() } + var typeWasSet = false + + if (javaType != null) { + typeWasSet = realEntry.setTypeIfMissing(javaType) + + if (realEntry.javaType != javaType) { + if (options.preferGraphQLResolver && realEntry.hasResolverRef()) { + log.warn("The real entry ${realEntry.joinReferences()} is a GraphQLResolver so ignoring this one ${javaType.unwrap()} $reference") + } else { + if (Map::class.java.isAssignableFrom(javaType.unwrap())) { + throw SchemaClassScannerError("Two different property map classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType}:\n| ${reference.getDescription()}") + } + throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType.typeName}:\n| ${reference.getDescription()}") + } + } + } + + realEntry.addReference(reference) + + // Check if we just added the entry... a little odd, but it works (and thread-safe, FWIW) + if (typeWasSet && javaType != null) { + handleNewType(type, javaType) + } + } + + /** + * Handle a newly found type, adding it to the list of actually used types and putting it in the scanning queue if it's an object type. + */ + private fun handleNewType(graphQLType: TypeDefinition<*>, javaType: JavaType) { + when (graphQLType) { + is ObjectTypeDefinition -> { + enqueue(graphQLType, javaType) + scanInterfacesOfType(graphQLType) + } + + is InputObjectTypeDefinition -> handleInputObjectFields(graphQLType, javaType) + } + } + + /** + * Finds the types of the input object's fields through the properties of its class. A null [javaType] means the input object + * has no known class, e.g. because it's only reachable through a Map. + */ + private fun handleInputObjectFields(graphQLType: InputObjectTypeDefinition, javaType: JavaType?) { + val clazz = javaType?.unwrap() + graphQLType.withExtensions(typeExtensions) + .flatMap { it.inputValueDefinitions } + .forEach { inputValueDefinition -> + val inputGraphQLType = inputValueDefinition.type.unwrap() + if (ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(inputGraphQLType.name)) { + return@forEach + } + + val propertyType = clazz?.let { findInputValueTypeInType(inputValueDefinition.name, it) } + val inputValueJavaType = propertyType ?: initialDictionary[inputGraphQLType.name]?.get() + if (inputValueJavaType == null) { + handleInputValueWithoutJavaType(graphQLType, inputValueDefinition, inputGraphQLType, clazz) + return@forEach + } + + handleFoundType(typeClassMatcher.match(TypeClassMatcher.PotentialMatch.parameterType( + // a class from the dictionary is for the unwrapped type, not for its list or non-null wrapper + if (propertyType != null) inputValueDefinition.type else inputGraphQLType, + inputValueJavaType, + GenericTypeResolver(javaType ?: inputValueJavaType, options), + InputObjectReference(inputValueDefinition) + ))) + } + } + + /** + * Without a Java property for an input field its type can still be built, as long as it doesn't need a class of its own. + * At runtime the value is whatever the input object's class (or the Map) deserializes it into. + */ + private fun handleInputValueWithoutJavaType(graphQLType: InputObjectTypeDefinition, inputValueDefinition: InputValueDefinition, inputGraphQLType: TypeName, clazz: Class<*>?) { + when (val definition = definitionsByName[inputGraphQLType.name]) { + is ScalarTypeDefinition -> handleFoundScalarType(definition) + is InputObjectTypeDefinition -> { + val isNewType = !dictionary.containsKey(definition) + // registered without a class, so a class found for it later still gets set and scanned + handleFoundType(definition, null, InputObjectReference(inputValueDefinition)) + if (isNewType) { + handleInputObjectFields(definition, null) + } + } + // the enum class may still be found through another field, so this is only reported once scanning is done + is EnumTypeDefinition -> enumsWithoutClass.putIfAbsent(definition, enumWithoutClassMessage(definition, graphQLType, inputValueDefinition, clazz)) + // undefined types and output types are reported by the SchemaParser + else -> {} + } + } + + // an input bound to Any gets a Map at runtime too + private fun isPropertyMap(clazz: Class<*>) = + Map::class.java.isAssignableFrom(clazz) || JsonNode::class.java.isAssignableFrom(clazz) || clazz == Any::class.java + + private fun isLibraryClass(clazz: Class<*>) = + listOf("java.", "kotlin.", "com.fasterxml.jackson.").any { clazz.name.startsWith(it) } + + private fun enumWithoutClassMessage(enumType: EnumTypeDefinition, graphQLType: InputObjectTypeDefinition, inputValueDefinition: InputValueDefinition, clazz: Class<*>?): String { + val field = inputValueDefinition.name + val reason = when { + clazz == null -> "no class was found for input type '${graphQLType.name}'" + isPropertyMap(clazz) && isLibraryClass(clazz) -> "'${graphQLType.name}' is bound to ${clazz.name}, which has no type information for its fields" + else -> "${clazz.name} has no public getter or field named '$field'" + } + return "Enum type '${enumType.name}' is used by field '$field' of input type '${graphQLType.name}', but its Java enum can't be determined because $reason. " + + "Pass the enum class for type '${enumType.name}' in the parser's dictionary, or use a class for '${graphQLType.name}' with a public getter or field '$field'." + } + + fun throwIfEnumWithoutClass() { + enumsWithoutClass.entries + .find { dictionary[it.key]?.javaType == null } + ?.let { throw SchemaClassScannerError(it.value) } + } + + fun scanInterfacesOfType(graphQLType: ObjectTypeDefinition) { + graphQLType.withExtensions(typeExtensions).flatMap { it.implements }.forEach { + if (it is TypeName) { + handleFoundType(interfaceDefinitionsByName[it.name] + ?: throw SchemaClassScannerError("Object type ${graphQLType.name} declared interface ${it.name}, but no interface with that name was found in the schema!"), null, InterfaceReference(graphQLType)) + } + } + } + + private fun findInputValueTypeInType(name: String, clazz: Class<*>): JavaType? { + // a Map or JsonNode has no properties for the input's fields, only methods that may share their names, e.g. ObjectNode.fields(), + // but a subclass of one may still declare typed getters or fields of its own + val isProperty = { member: Member -> !isPropertyMap(clazz) || !isLibraryClass(member.declaringClass) } + val methods = clazz.methods.filter(isProperty) + + val (getters, methodsWithParameters) = methods.filter { + it.name == name || it.name == "get${name.replaceFirstChar(Char::titlecase)}" + }.sortedBy { it.name.length }.partition { it.parameterCount == 0 } + + return getters.find { !it.isSynthetic }?.genericReturnType + ?: getters.firstOrNull()?.genericReturnType + ?: clazz.fields.find { it.name == name && isProperty(it) }?.genericType + ?: methodsWithParameters.find { !it.isSynthetic }?.genericReturnType + ?: methodsWithParameters.firstOrNull()?.genericReturnType + } + + interface EntryView { + val javaType: JavaType? + fun typeClass(): Class? + } + + class DictionaryEntry : EntryView { + private val references = mutableListOf() + override var javaType: JavaType? = null + private set + + fun setTypeIfMissing(javaType: JavaType): Boolean { + if (this.javaType == null) { + this.javaType = javaType + return true + } + + return false + } + +// fun type(): JavaType? = classEntry?.type + + override fun typeClass(): Class? = javaType?.unwrap() + + fun addReference(reference: Reference) { + references.add(reference) + } + + fun joinReferences() = "- ${javaType?.typeName}:\n| " + references.joinToString("\n| ") { it.getDescription() } + + fun hasResolverRef(): Boolean { + references.filterIsInstance().forEach { reference -> + if (GraphQLResolver::class.java.isAssignableFrom(reference.getMethod().declaringClass)) { + return true + } + } + return false + } + } + + class InitialDictionaryEntry(private val clazz: Class<*>) { + var accessed = false + private set + + fun get(): Class<*> { + accessed = true + return clazz + } + } + + abstract class Reference { + abstract fun getDescription(): String + override fun toString() = getDescription() + } + + class DictionaryReference : Reference() { + override fun getDescription() = "provided dictionary" + } + + class UnusedTypeReference : Reference() { + override fun getDescription() = "unused type" + } + + private class InterfaceReference(private val type: ObjectTypeDefinition) : Reference() { + override fun getDescription() = "interface declarations of ${type.name}" + } + + class UnionMemberReference(private val union: UnionTypeDefinition) : Reference() { + override fun getDescription() = "member of union ${union.name}" + } + + class SealedSubclassReference(private val sealedType: JavaType) : Reference() { + override fun getDescription() = "subclass of sealed type ${sealedType.typeName}" + } + + private class InputObjectReference(private val type: InputValueDefinition) : Reference() { + override fun getDescription() = "input object $type" + } + + internal class ReturnValueReference(private val method: Method) : Reference() { + fun getMethod() = method + override fun getDescription() = "return type of method $method" + } + + internal class MethodParameterReference(private val method: Method, private val index: Int) : Reference() { + override fun getDescription() = "parameter $index of method $method" + } + + internal class FieldTypeReference(private val field: String) : Reference() { + override fun getDescription() = "type of field $field" + } +} diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index 55da96b4..7ee3c42a 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -1,6 +1,10 @@ package graphql.kickstart.tools -import com.fasterxml.jackson.databind.JsonNode +import graphql.kickstart.tools.ScanDictionary.DictionaryReference +import graphql.kickstart.tools.ScanDictionary.FieldTypeReference +import graphql.kickstart.tools.ScanDictionary.InitialDictionaryEntry +import graphql.kickstart.tools.ScanDictionary.UnionMemberReference +import graphql.kickstart.tools.ScanDictionary.UnusedTypeReference import graphql.kickstart.tools.resolver.FieldResolver import graphql.kickstart.tools.resolver.FieldResolverScanner import graphql.kickstart.tools.resolver.MethodFieldResolver @@ -10,13 +14,10 @@ import graphql.language.* import graphql.schema.idl.ScalarInfo import graphql.schema.idl.TypeUtil import org.apache.commons.lang3.ClassUtils -import org.apache.commons.lang3.reflect.TypeUtils import org.slf4j.LoggerFactory import java.lang.reflect.Field import java.lang.reflect.Member -import java.lang.reflect.Method import java.lang.reflect.Modifier -import java.lang.reflect.ParameterizedType /** * @author Andrew Potter @@ -55,15 +56,14 @@ internal class SchemaClassScanner( .groupBy { it.name } private val objectDefinitions = allDefinitions.filterIsInstance().filter { it !is ObjectTypeExtensionDefinition } private val objectDefinitionsByName = objectDefinitions.associateBy { it.name } - private val interfaceDefinitionsByName = definitionsByName.values.filterIsInstance().associateBy { it.name } private val scalarNames = ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.keys + scalarDefinitions.map { it.name } private val fieldResolverScanner = FieldResolverScanner(options, scalarNames) private val typeClassMatcher = TypeClassMatcher(definitionsByName) - private val dictionary = mutableMapOf, DictionaryEntry>() private val unvalidatedTypes = mutableSetOf>() - private val enumsWithoutClass = mutableMapOf() private val queue = linkedSetOf() + private val dictionary = ScanDictionary(options, log, typeExtensions, definitionsByName, typeClassMatcher, this.initialDictionary, ::enqueue, ::handleFoundScalarType) + private val sealedSubclassMatcher = SealedSubclassMatcher(dictionary, typeExtensions) private val fieldResolversByType = mutableMapOf>() @@ -158,9 +158,9 @@ internal class SchemaClassScanner( private fun handleUnusedType(type: TypeDefinition<*>) { val initialEntry = initialDictionary[type.name] when { - initialEntry != null -> handleFoundType(type, initialEntry.get(), DictionaryReference()) + initialEntry != null -> dictionary.handleFoundType(type, initialEntry.get(), DictionaryReference()) // Interfaces and unions are resolved through the classes of their implementations, so they don't need one - type is InterfaceTypeDefinition || type is UnionTypeDefinition -> handleFoundType(type, null, UnusedTypeReference()) + type is InterfaceTypeDefinition || type is UnionTypeDefinition -> dictionary.handleFoundType(type, null, UnusedTypeReference()) else -> throw SchemaClassScannerError("Object type '${type.name}' is unused and includeUnusedTypes is true. Please pass a class for type '${type.name}' in the parser's dictionary.") } } @@ -186,7 +186,7 @@ internal class SchemaClassScanner( } unvalidatedTypes.add(rootType.type) - scanInterfacesOfType(rootType.type) + dictionary.scanInterfacesOfType(rootType.type) scanResolverInfoForPotentialMatches(rootType.type, rootType.resolverInfo) } @@ -225,7 +225,7 @@ internal class SchemaClassScanner( val initialEntry = initialDictionary[type.name] if (initialEntry != null) { - handleFoundType(type, initialEntry.get(), DictionaryReference()) + dictionary.handleFoundType(type, initialEntry.get(), DictionaryReference()) } else { unvalidatedTypes.add(type) } @@ -251,9 +251,7 @@ internal class SchemaClassScanner( } private fun validateAndCreateResult(rootTypeHolder: RootTypesHolder): ScannedSchemaObjects { - enumsWithoutClass.entries - .find { dictionary[it.key]?.javaType == null } - ?.let { throw SchemaClassScannerError(it.value) } + dictionary.throwIfEnumWithoutClass() initialDictionary .filter { !it.value.accessed } @@ -361,7 +359,7 @@ internal class SchemaClassScanner( .map { it to UnionMemberReference(union) } } - newUnions.forEach { (type, reference) -> handleFoundType(type, null, reference) } + newUnions.forEach { (type, reference) -> dictionary.handleFoundType(type, null, reference) } return newUnions.isNotEmpty() } @@ -381,7 +379,7 @@ internal class SchemaClassScanner( } } - newTypes.forEach { (type, reference) -> handleFoundType(type, null, reference) } + newTypes.forEach { (type, reference) -> dictionary.handleFoundType(type, null, reference) } return newTypes.isNotEmpty() } @@ -392,75 +390,19 @@ internal class SchemaClassScanner( if (!unvalidatedTypes.contains(type) && !dictionaryContainsType) { val initialEntry = initialDictionary[type.name] if (initialEntry != null) { - handleFoundType(type, initialEntry.get(), DictionaryReference()) + dictionary.handleFoundType(type, initialEntry.get(), DictionaryReference()) } else { typesWithoutClass.add(type) } } } - val sealedMatches = typesWithoutClass.filterIsInstance().mapNotNull { sealedSubclassMatches(it).singleOrNull() } + val sealedMatches = typesWithoutClass.filterIsInstance().mapNotNull { sealedSubclassMatcher.sealedSubclassMatches(it).singleOrNull() } if (sealedMatches.isEmpty()) { - typesWithoutClass.firstOrNull()?.let { throw SchemaClassScannerError(failureMessage(it) + sealedSubclassHint(it)) } + typesWithoutClass.firstOrNull()?.let { throw SchemaClassScannerError(failureMessage(it) + sealedSubclassMatcher.sealedSubclassHint(it)) } } - sealedMatches.forEach { handleFoundType(it) } - } - - private fun sealedSubclassHint(type: TypeDefinition<*>): String { - if (type !is ObjectTypeDefinition) { - return "" - } - - val sealedTypes = sealedTypesOf(type) - if (sealedTypes.isEmpty()) { - return "" - } - - val sealedTypeNames = sealedTypes.joinToString(" or ") { it.unwrap().name } - val matches = sealedSubclassMatches(type) - return if (matches.isEmpty()) { - " No subclass of sealed type $sealedTypeNames is named '${type.name}'." - } else { - " More than one subclass of sealed type $sealedTypeNames is named '${type.name}': ${matches.joinToString { it.javaType.unwrap().name }}." - } - } - - private fun sealedSubclassMatches(type: ObjectTypeDefinition): List = - sealedTypesOf(type) - .flatMap { sealedType -> - sealedSubclasses(sealedType.unwrap()) - .filter { it.simpleName == type.name } - .map { TypeClassMatcher.ValidMatch(type, withSealedTypeArguments(it, sealedType), SealedSubclassReference(sealedType)) } - } - .distinctBy { it.javaType.unwrap() } - - private fun sealedTypesOf(type: ObjectTypeDefinition): List = - dictionary - .filterKeys { definition -> - when (definition) { - is UnionTypeDefinition -> definition.withExtensions(typeExtensions).flatMap { it.memberTypes }.filterIsInstance().any { it.name == type.name } - is InterfaceTypeDefinition -> type.withExtensions(typeExtensions).flatMap { it.implements }.filterIsInstance().any { it.name == definition.name } - else -> false - } - } - .values.mapNotNull { it.javaType } - .distinct() - .filter { it.unwrap().kotlin.isSealed } - - // kotlin-reflect also returns the permitted subclasses of Java sealed classes - private fun sealedSubclasses(clazz: Class<*>): List> = - clazz.kotlin.sealedSubclasses.flatMap { listOf(it.java) + sealedSubclasses(it.java) } - - private fun withSealedTypeArguments(subclass: Class<*>, sealedType: JavaType): JavaType { - if (sealedType !is ParameterizedType || subclass.typeParameters.isEmpty()) { - return subclass - } - - return TypeUtils.determineTypeArguments(subclass, sealedType) - ?.takeIf { it.keys.containsAll(subclass.typeParameters.asList()) } - ?.let { TypeUtils.parameterize(subclass, it) } - ?: subclass + sealedMatches.forEach { dictionary.handleFoundType(it) } } /** @@ -513,7 +455,7 @@ internal class SchemaClassScanner( fieldResolversByType.getOrPut(type) { mutableMapOf() }[fieldResolver.field] = fieldResolver fieldResolver.scanForMatches().forEach { potentialMatch -> - handleFoundType(typeClassMatcher.match(potentialMatch)) + dictionary.handleFoundType(typeClassMatcher.match(potentialMatch)) } warnIfParameterMissing(type, fieldResolver) } @@ -561,139 +503,10 @@ internal class SchemaClassScanner( } } - private fun handleFoundType(match: TypeClassMatcher.Match) = when (match) { - is TypeClassMatcher.ScalarMatch -> handleFoundScalarType(match.type) - is TypeClassMatcher.ValidMatch -> handleFoundType(match.type, match.javaType, match.reference) - } - private fun handleFoundScalarType(type: ScalarTypeDefinition) { unvalidatedTypes.add(type) } - /** - * Enter a found type into the dictionary if it doesn't exist yet, add a reference pointing back to where it was discovered. - */ - private fun handleFoundType(type: TypeDefinition<*>, javaType: JavaType?, reference: Reference) { - val realEntry = dictionary.getOrPut(type) { DictionaryEntry() } - var typeWasSet = false - - if (javaType != null) { - typeWasSet = realEntry.setTypeIfMissing(javaType) - - if (realEntry.javaType != javaType) { - if (options.preferGraphQLResolver && realEntry.hasResolverRef()) { - log.warn("The real entry ${realEntry.joinReferences()} is a GraphQLResolver so ignoring this one ${javaType.unwrap()} $reference") - } else { - if (Map::class.java.isAssignableFrom(javaType.unwrap())) { - throw SchemaClassScannerError("Two different property map classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType}:\n| ${reference.getDescription()}") - } - throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType.typeName}:\n| ${reference.getDescription()}") - } - } - } - - realEntry.addReference(reference) - - // Check if we just added the entry... a little odd, but it works (and thread-safe, FWIW) - if (typeWasSet && javaType != null) { - handleNewType(type, javaType) - } - } - - /** - * Handle a newly found type, adding it to the list of actually used types and putting it in the scanning queue if it's an object type. - */ - private fun handleNewType(graphQLType: TypeDefinition<*>, javaType: JavaType) { - when (graphQLType) { - is ObjectTypeDefinition -> { - enqueue(graphQLType, javaType) - scanInterfacesOfType(graphQLType) - } - - is InputObjectTypeDefinition -> handleInputObjectFields(graphQLType, javaType) - } - } - - /** - * Finds the types of the input object's fields through the properties of its class. A null [javaType] means the input object - * has no known class, e.g. because it's only reachable through a Map. - */ - private fun handleInputObjectFields(graphQLType: InputObjectTypeDefinition, javaType: JavaType?) { - val clazz = javaType?.unwrap() - graphQLType.withExtensions(typeExtensions) - .flatMap { it.inputValueDefinitions } - .forEach { inputValueDefinition -> - val inputGraphQLType = inputValueDefinition.type.unwrap() - if (ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(inputGraphQLType.name)) { - return@forEach - } - - val propertyType = clazz?.let { findInputValueTypeInType(inputValueDefinition.name, it) } - val inputValueJavaType = propertyType ?: initialDictionary[inputGraphQLType.name]?.get() - if (inputValueJavaType == null) { - handleInputValueWithoutJavaType(graphQLType, inputValueDefinition, inputGraphQLType, clazz) - return@forEach - } - - handleFoundType(typeClassMatcher.match(TypeClassMatcher.PotentialMatch.parameterType( - // a class from the dictionary is for the unwrapped type, not for its list or non-null wrapper - if (propertyType != null) inputValueDefinition.type else inputGraphQLType, - inputValueJavaType, - GenericTypeResolver(javaType ?: inputValueJavaType, options), - InputObjectReference(inputValueDefinition) - ))) - } - } - - /** - * Without a Java property for an input field its type can still be built, as long as it doesn't need a class of its own. - * At runtime the value is whatever the input object's class (or the Map) deserializes it into. - */ - private fun handleInputValueWithoutJavaType(graphQLType: InputObjectTypeDefinition, inputValueDefinition: InputValueDefinition, inputGraphQLType: TypeName, clazz: Class<*>?) { - when (val definition = definitionsByName[inputGraphQLType.name]) { - is ScalarTypeDefinition -> handleFoundScalarType(definition) - is InputObjectTypeDefinition -> { - val isNewType = !dictionary.containsKey(definition) - // registered without a class, so a class found for it later still gets set and scanned - handleFoundType(definition, null, InputObjectReference(inputValueDefinition)) - if (isNewType) { - handleInputObjectFields(definition, null) - } - } - // the enum class may still be found through another field, so this is only reported once scanning is done - is EnumTypeDefinition -> enumsWithoutClass.putIfAbsent(definition, enumWithoutClassMessage(definition, graphQLType, inputValueDefinition, clazz)) - // undefined types and output types are reported by the SchemaParser - else -> {} - } - } - - // an input bound to Any gets a Map at runtime too - private fun isPropertyMap(clazz: Class<*>) = - Map::class.java.isAssignableFrom(clazz) || JsonNode::class.java.isAssignableFrom(clazz) || clazz == Any::class.java - - private fun isLibraryClass(clazz: Class<*>) = - listOf("java.", "kotlin.", "com.fasterxml.jackson.").any { clazz.name.startsWith(it) } - - private fun enumWithoutClassMessage(enumType: EnumTypeDefinition, graphQLType: InputObjectTypeDefinition, inputValueDefinition: InputValueDefinition, clazz: Class<*>?): String { - val field = inputValueDefinition.name - val reason = when { - clazz == null -> "no class was found for input type '${graphQLType.name}'" - isPropertyMap(clazz) && isLibraryClass(clazz) -> "'${graphQLType.name}' is bound to ${clazz.name}, which has no type information for its fields" - else -> "${clazz.name} has no public getter or field named '$field'" - } - return "Enum type '${enumType.name}' is used by field '$field' of input type '${graphQLType.name}', but its Java enum can't be determined because $reason. " + - "Pass the enum class for type '${enumType.name}' in the parser's dictionary, or use a class for '${graphQLType.name}' with a public getter or field '$field'." - } - - private fun scanInterfacesOfType(graphQLType: ObjectTypeDefinition) { - graphQLType.withExtensions(typeExtensions).flatMap { it.implements }.forEach { - if (it is TypeName) { - handleFoundType(interfaceDefinitionsByName[it.name] - ?: throw SchemaClassScannerError("Object type ${graphQLType.name} declared interface ${it.name}, but no interface with that name was found in the schema!"), null, InterfaceReference(graphQLType)) - } - } - } - private fun extendsOtherKind(definition: TypeDefinition<*>): Boolean { val type = definitionsByName[definition.name] return definition is SDLExtensionDefinition && type != null && type.kind() != definition.kind() @@ -713,111 +526,8 @@ internal class SchemaClassScanner( queue.add(QueueItem(graphQLType, javaType)) } - private fun findInputValueTypeInType(name: String, clazz: Class<*>): JavaType? { - // a Map or JsonNode has no properties for the input's fields, only methods that may share their names, e.g. ObjectNode.fields(), - // but a subclass of one may still declare typed getters or fields of its own - val isProperty = { member: Member -> !isPropertyMap(clazz) || !isLibraryClass(member.declaringClass) } - val methods = clazz.methods.filter(isProperty) - - val (getters, methodsWithParameters) = methods.filter { - it.name == name || it.name == "get${name.replaceFirstChar(Char::titlecase)}" - }.sortedBy { it.name.length }.partition { it.parameterCount == 0 } - - return getters.find { !it.isSynthetic }?.genericReturnType - ?: getters.firstOrNull()?.genericReturnType - ?: clazz.fields.find { it.name == name && isProperty(it) }?.genericType - ?: methodsWithParameters.find { !it.isSynthetic }?.genericReturnType - ?: methodsWithParameters.firstOrNull()?.genericReturnType - } - private data class QueueItem(val type: ObjectTypeDefinition, val clazz: JavaType) - private class DictionaryEntry { - private val references = mutableListOf() - var javaType: JavaType? = null - private set - - fun setTypeIfMissing(javaType: JavaType): Boolean { - if (this.javaType == null) { - this.javaType = javaType - return true - } - - return false - } - -// fun type(): JavaType? = classEntry?.type - - fun typeClass(): Class? = javaType?.unwrap() - - fun addReference(reference: Reference) { - references.add(reference) - } - - fun joinReferences() = "- ${javaType?.typeName}:\n| " + references.joinToString("\n| ") { it.getDescription() } - - fun hasResolverRef(): Boolean { - references.filterIsInstance().forEach { reference -> - if (GraphQLResolver::class.java.isAssignableFrom(reference.getMethod().declaringClass)) { - return true - } - } - return false - } - } - - abstract class Reference { - abstract fun getDescription(): String - override fun toString() = getDescription() - } - - private class DictionaryReference : Reference() { - override fun getDescription() = "provided dictionary" - } - - private class UnusedTypeReference : Reference() { - override fun getDescription() = "unused type" - } - - private class InterfaceReference(private val type: ObjectTypeDefinition) : Reference() { - override fun getDescription() = "interface declarations of ${type.name}" - } - - private class UnionMemberReference(private val union: UnionTypeDefinition) : Reference() { - override fun getDescription() = "member of union ${union.name}" - } - - private class SealedSubclassReference(private val sealedType: JavaType) : Reference() { - override fun getDescription() = "subclass of sealed type ${sealedType.typeName}" - } - - private class InputObjectReference(private val type: InputValueDefinition) : Reference() { - override fun getDescription() = "input object $type" - } - - private class InitialDictionaryEntry(private val clazz: Class<*>) { - var accessed = false - private set - - fun get(): Class<*> { - accessed = true - return clazz - } - } - - internal class ReturnValueReference(private val method: Method) : Reference() { - fun getMethod() = method - override fun getDescription() = "return type of method $method" - } - - internal class MethodParameterReference(private val method: Method, private val index: Int) : Reference() { - override fun getDescription() = "parameter $index of method $method" - } - - internal class FieldTypeReference(private val field: String) : Reference() { - override fun getDescription() = "type of field $field" - } - private class RootTypesHolder( options: SchemaParserOptions, rootInfo: RootTypeInfo, diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt index cb640174..acfd9686 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt @@ -1,7 +1,6 @@ package graphql.kickstart.tools import graphql.Directives -import graphql.Scalars import graphql.introspection.Introspection import graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION import graphql.introspection.Introspection.DirectiveLocation.ENUM @@ -12,10 +11,10 @@ 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 import graphql.kickstart.tools.resolver.MethodFieldResolver +import graphql.kickstart.tools.util.describe import graphql.kickstart.tools.util.extensionsOf import graphql.kickstart.tools.util.getDocumentation import graphql.kickstart.tools.util.unwrap @@ -69,12 +68,12 @@ class SchemaParser internal constructor( unionDefinitions.map { it.name }).toSet() private val permittedTypesForInputObject: Set = (inputObjectDefinitions.map { it.name } + enumDefinitions.map { it.name }).toSet() + private val typeReferenceErrors = TypeReferenceErrors(definitions, unusedDefinitions, typeExtensions) private val codeRegistryBuilder = GraphQLCodeRegistry.newCodeRegistry() private val directiveWiringHelper = DirectiveWiringHelper(runtimeWiring, codeRegistryBuilder) - // by name, in the order they're declared - private lateinit var schemaDirectives: MutableMap + private lateinit var sdlDirectives: SdlDirectives /** * Parses the given schema with respect to the given dictionary and returns GraphQL objects. @@ -91,7 +90,10 @@ class SchemaParser internal constructor( // Create GraphQL objects val inputObjects: MutableList = mutableListOf() - createDirectives() + sdlDirectives = SdlDirectives(directiveDefinitions, options, runtimeWiring, log) { + // type references, so that an input object used by a directive applied to it doesn't contain itself + determineType(GraphQLInputType::class, it, permittedTypesForInputObject) as GraphQLInputType + } val scalars = scalarDefinitions.values.map { createScalar(it) } inputObjectDefinitions.forEach { if (inputObjects.none { io -> io.name == it.name }) { @@ -124,8 +126,8 @@ class SchemaParser internal constructor( val types = (additionalObjects.toSet() as Set) + inputObjects + enums + interfaces + unions + scalars // Directives applied to the schema itself, e.g. `extend schema @link(...)`. Only GraphQLAppliedDirectives are built here, not // the deprecated GraphQLDirective ones: graphql-java doesn't resolve type references in those, so enum arguments would fail. - val schemaAppliedDirectives = appliedDirectives(resolveDirectives(rootInfo.getDirectives(), SCHEMA)).toList() - val schemaObjects = SchemaObjects(query, mutation, subscription, types, schemaDirectives.values.toSet(), codeRegistryBuilder, rootInfo.getDescription(), schemaAppliedDirectives) + val schemaAppliedDirectives = sdlDirectives.schemaAppliedDirectives(rootInfo.getDirectives()) + val schemaObjects = SchemaObjects(query, mutation, subscription, types, sdlDirectives.definitions.toSet(), codeRegistryBuilder, rootInfo.getDescription(), schemaAppliedDirectives) // in the order the types were created val wiredObjects = if (directiveWiringHelper.isWiringRequired) wire(schemaObjects, scalars + inputObjects + interfaces + objects + unions + enums) else schemaObjects if (options.unmappedInputFields != UnmappedInputFields.IGNORE) { @@ -168,7 +170,7 @@ class SchemaParser internal constructor( private fun createObject(objectDefinition: ObjectTypeDefinition, interfaces: List, inputObjects: List): GraphQLObjectType { val name = objectDefinition.name val extensions = typeExtensions.extensionsOf(name) - val directives = directivesOf(objectDefinition, extensions) + val directives = sdlDirectives.directivesOf(objectDefinition, extensions) val builder = GraphQLObjectType.newObject() .name(name) .definition(objectDefinition) @@ -206,7 +208,7 @@ class SchemaParser internal constructor( private fun createInputObject(definition: InputObjectTypeDefinition, inputObjects: List, inputObjectsInProgress: MutableSet): GraphQLInputObjectType { val extensionDefinitions = typeExtensions.extensionsOf(definition.name) - val directives = directivesOf(definition, extensionDefinitions) + val directives = sdlDirectives.directivesOf(definition, extensionDefinitions) inputObjectsInProgress.add(definition.name) @@ -225,7 +227,7 @@ class SchemaParser internal constructor( .definition(fieldDefinition) .description(getDocumentation(fieldDefinition, options)) .apply { fieldDefinition.defaultValue?.let { v -> defaultValueLiteral(v) } } - .apply { getDeprecated(fieldDefinition.directives)?.let { deprecate(it) } } + .apply { sdlDirectives.getDeprecated(fieldDefinition.directives)?.let { deprecate(it) } } .type(determineInputType(fieldDefinition.type, inputObjects, inputObjectsInProgress)) .withSdlDirectives(fieldDefinition.directives, INPUT_FIELD_DEFINITION) .build() @@ -245,7 +247,7 @@ class SchemaParser internal constructor( if (enumClass != null && !enumClass.isEnum) throw SchemaError("Type '$name' is declared as an enum in the GraphQL schema but is not a Java enum!") val extensions = typeExtensions.extensionsOf(name) - val directives = directivesOf(definition, extensions) + val directives = sdlDirectives.directivesOf(definition, extensions) val values = definition.withExtensions(typeExtensions).flatMap { it.enumValueDefinitions } warnRepeated(name, "value", values.map { it.name }) val builder = GraphQLEnumType.newEnum() @@ -269,7 +271,7 @@ class SchemaParser internal constructor( .name(enumName) .description(getDocumentation(valueDefinition, options)) .value(enumValue) - .apply { getDeprecated(valueDefinition.directives)?.let { deprecationReason(it) } } + .apply { sdlDirectives.getDeprecated(valueDefinition.directives)?.let { deprecationReason(it) } } .withSdlDirectives(valueDefinition.directives, ENUM_VALUE) .definition(valueDefinition) .build() @@ -284,7 +286,7 @@ class SchemaParser internal constructor( val name = definition.name val provided = customScalars.getValue(name) val extensions = typeExtensions.extensionsOf(name) - val directives = directivesOf(definition, extensions) + val directives = sdlDirectives.directivesOf(definition, extensions) val builder = GraphQLScalarType.newScalar(provided) .definition(definition) .extensionDefinitions(extensions) @@ -304,7 +306,7 @@ class SchemaParser internal constructor( private fun createInterfaceObject(interfaceDefinition: InterfaceTypeDefinition, inputObjects: List): GraphQLInterfaceType { val extensions = typeExtensions.extensionsOf(interfaceDefinition.name) - val directives = directivesOf(interfaceDefinition, extensions) + val directives = sdlDirectives.directivesOf(interfaceDefinition, extensions) val fieldDefinitions = interfaceDefinition.withExtensions(typeExtensions).flatMap { it.fieldDefinitions } warnRepeated(interfaceDefinition.name, "field", fieldDefinitions.map { it.name }) val builder = GraphQLInterfaceType.newInterface() @@ -330,7 +332,7 @@ class SchemaParser internal constructor( private fun createUnionObject(definition: UnionTypeDefinition, types: List): GraphQLUnionType { val extensions = typeExtensions.extensionsOf(definition.name) - val directives = directivesOf(definition, extensions) + val directives = sdlDirectives.directivesOf(definition, extensions) warnRepeated(definition.name, "member", definition.withExtensions(typeExtensions).flatMap { it.memberTypes }.map { (it as TypeName).name }) val builder = GraphQLUnionType.newUnionType() .name(definition.name) @@ -380,25 +382,12 @@ class SchemaParser internal constructor( } } - private fun directivesOf(definition: TypeDefinition<*>, extensions: List>): List { - val applied = definition.directives.mapTo(mutableSetOf()) { it.name } - return definition.directives + extensions.flatMap { extension -> - val (repeated, kept) = extension.directives.partition { it.name in applied && isNonRepeatableByFirstDeclaration(it.name) } - repeated.forEach { log.warn("Directive @${it.name} isn't repeatable but an extension of type '${definition.name}' applies it again, only the first one is kept.") } - applied += kept.map { it.name } - kept - } - } - - private fun isNonRepeatableByFirstDeclaration(directiveName: String): Boolean = - directiveDefinitions.find { it.name == directiveName }?.let { !it.isRepeatable } ?: BUILT_IN_DIRECTIVES.containsKey(directiveName) - private fun createField(field: GraphQLFieldDefinition.Builder, fieldDefinition: FieldDefinition, inputObjects: List): GraphQLFieldDefinition.Builder { return field .name(fieldDefinition.name) .description(getDocumentation(fieldDefinition, options)) .definition(fieldDefinition) - .apply { getDeprecated(fieldDefinition.directives)?.let { deprecate(it) } } + .apply { sdlDirectives.getDeprecated(fieldDefinition.directives)?.let { deprecate(it) } } .type(determineOutputType(fieldDefinition.type)) .withSdlDirectives(fieldDefinition.directives, FIELD_DEFINITION) .apply { @@ -414,193 +403,14 @@ class SchemaParser internal constructor( .definition(definition) .description(getDocumentation(definition, options)) .type(determineInputType(definition.type, inputObjects, mutableSetOf())) - .apply { getDeprecated(definition.directives)?.let { deprecate(it) } } + .apply { sdlDirectives.getDeprecated(definition.directives)?.let { deprecate(it) } } .apply { definition.defaultValue?.let { defaultValueLiteral(it) } } .withSdlDirectives(definition.directives, ARGUMENT_DEFINITION) .build() } - private fun createDirectives() { - schemaDirectives = directiveDefinitions.associateTo(linkedMapOf()) { it.name to createDirective(it) } - // because the arguments can have directives too, we attach them only after the directives themselves are created - schemaDirectives.replaceAll { _, d -> - val arguments = d.arguments.map { a -> a.transform { - it.withSdlDirectives(a.definition!!.directives, ARGUMENT_DEFINITION) - } } - d.transform { it.replaceArguments(arguments) } - } - } - - private fun createDirective(definition: DirectiveDefinition): GraphQLDirective { - val locations = definition.directiveLocations.map { Introspection.DirectiveLocation.valueOf(it.name) }.toTypedArray() - - return GraphQLDirective.newDirective() - .name(definition.name) - .description(getDocumentation(definition, options)) - .definition(definition) - .comparatorRegistry(runtimeWiring.comparatorRegistry) - .validLocations(*locations) - .repeatable(definition.isRepeatable) - .apply { - definition.inputValueDefinitions.forEach { argumentDefinition -> - argument(createDirectiveArgument(argumentDefinition)) - } - } - .build() - } - - private fun createDirectiveArgument(definition: InputValueDefinition): GraphQLArgument { - return GraphQLArgument.newArgument() - .name(definition.name) - .definition(definition) - .description(getDocumentation(definition, options)) - // type references, so that an input object used by a directive applied to it doesn't contain itself - .type(determineType(GraphQLInputType::class, definition.type, permittedTypesForInputObject) as GraphQLInputType) - .apply { getDeprecated(definition.directives)?.let { deprecate(it) } } - .apply { definition.defaultValue?.let { defaultValueLiteral(it) } } - .build() - } - - @Suppress("DEPRECATION") - private fun > B.withSdlDirectives(directives: List, location: Introspection.DirectiveLocation): B { - if (directives.isEmpty()) return this - val resolved = resolveDirectives(directives, location) - val applied = appliedDirectives(resolved) - val legacy = legacyDirectives(resolved, location) - return withAppliedDirectives(*applied).withDirectives(*legacy) - } - - private fun resolveDirectives(directives: List, location: Introspection.DirectiveLocation): List { - // TODO throw a SchemaError instead in 15.0.0 (#884), like graphql-java does - directiveProblems(directives, location).forEach { log.warn(it) } - return directives.map { directive -> - val definition = findDirective(directive.name) - when { - definition != null -> ResolvedDirective(directive, declaredArguments(directive, definition), declared = true) - options.allowUndeclaredDirectives -> ResolvedDirective(directive, undeclaredArguments(directive), declared = false) - else -> throw SchemaError("Found applied directive ${directive.name} without corresponding directive definition.") - } - } - } - - // missing arguments get their defaults like in graphql-java, which gives a bare @deprecated the "reason" SchemaPrinter needs - private fun declaredArguments(directive: Directive, definition: GraphQLDirective): List { - val supplied = directive.arguments.map { arg -> - val argument = definition.getArgument(arg.name) - ?: throw SchemaError("Found an unexpected directive argument ${directive.name}#${arg.name} .") - ResolvedArgument(arg.name, argument.type, InputValueWithState.newLiteralValue(arg.value), legacyDescription = null) - } - val defaulted = missingArgumentsWithDefault(directive, definition).map { - ResolvedArgument(it.name, it.type, it.argumentDefaultValue, legacyDescription = it.description) - } - return supplied + defaulted - } - - private fun undeclaredArguments(directive: Directive): List = - directive.arguments.map { arg -> - ResolvedArgument(arg.name, guessDirectiveArgumentType(directive, arg.name, arg.value), InputValueWithState.newLiteralValue(arg.value), legacyDescription = null) - } - - private fun directiveProblems(directives: List, location: Introspection.DirectiveLocation): List = - directives.groupBy { it.name }.flatMap { (name, applied) -> - // undeclared directives have no definition to check against - val definition = findDirective(name) ?: return@flatMap emptyList() - buildList { - if (location !in definition.validLocations()) { - add("Directive @$name${applied.first().sourceLocation.describe()} is applied to $location, but it's declared on ${definition.validLocations().joinToString(" | ")}.") - } - if (definition.isNonRepeatable && applied.size > 1) { - add("Directive @$name${applied[1].sourceLocation.describe()} isn't repeatable but is applied more than once.") - } - applied.forEach { directive -> - definition.arguments - .filter { it.type is GraphQLNonNull && !it.hasSetDefaultValue() && directive.getArgument(it.name) == null } - .forEach { add("Directive @$name${directive.sourceLocation.describe()} is missing its required argument '${it.name}'.") } - } - } - } - - private fun findDirective(name: String): GraphQLDirective? = schemaDirectives[name] ?: BUILT_IN_DIRECTIVES[name] - - // there's no directive definition to look the type up in, so it's guessed from the value - private fun guessDirectiveArgumentType(directive: Directive, argumentName: String, value: Value<*>): GraphQLInputType { - return when (value) { - is StringValue, is NullValue -> Scalars.GraphQLString - is IntValue -> Scalars.GraphQLInt - is FloatValue -> Scalars.GraphQLFloat - is BooleanValue -> Scalars.GraphQLBoolean - is ArrayValue -> GraphQLList(value.values.firstOrNull()?.let { guessDirectiveArgumentType(directive, argumentName, it) } ?: Scalars.GraphQLString) - else -> throw SchemaError("Can't guess the type of argument ${directive.name}#$argumentName of undeclared directive ${directive.name}, please declare the directive.") - } - } - - // graphql-java's applied directives and their arguments have no description - private fun appliedDirectives(directives: List): Array = - directives.map { directive -> - GraphQLAppliedDirective.newDirective() - .name(directive.usage.name) - .definition(directive.usage) - .comparatorRegistry(runtimeWiring.comparatorRegistry) - .apply { - directive.arguments.forEach { arg -> - argument(GraphQLAppliedDirectiveArgument.newArgument() - .name(arg.name) - .type(arg.type) - .inputValueWithState(arg.value) - .build() - ) - } - } - .build() - }.toTypedArray() - - // TODO remove this once directives are fully replaced with applied directives - @Suppress("DEPRECATION") // legacy directive argument values - private fun legacyDirectives(directives: List, location: Introspection.DirectiveLocation): Array { - val seen = mutableSetOf() - return directives.mapNotNull { directive -> - // graphql-java rejects legacy directives without a definition, so undeclared ones are only kept as applied directives - if (!directive.declared) return@mapNotNull null - val name = directive.usage.name - val nonRepeatable = isNonRepeatableByFirstDeclaration(name) - // graphql-java rejects a non-repeatable legacy directive that is applied more than once - if (!seen.add(name) && nonRepeatable) return@mapNotNull null - GraphQLDirective.newDirective() - .name(name) - .description(getDocumentation(directive.usage, options)) - .comparatorRegistry(runtimeWiring.comparatorRegistry) - .validLocation(location) - .repeatable(!nonRepeatable) - .apply { - directive.arguments.forEach { arg -> - argument(GraphQLArgument.newArgument() - .name(arg.name) - .type(arg.type) - .description(arg.legacyDescription) - // there's no InputValueWithState setter. only a default can be programmatic, like @deprecated's reason - .apply { if (arg.value.isLiteral) valueLiteral(arg.value.value as Value<*>) else valueProgrammatic(arg.value.value) } - .build()) - } - } - .build() - }.toTypedArray() - } - - private class ResolvedDirective( - val usage: Directive, - val arguments: List, - val declared: Boolean - ) - - private class ResolvedArgument( - val name: String, - val type: GraphQLInputType, - val value: InputValueWithState, - val legacyDescription: String? - ) - - private fun missingArgumentsWithDefault(directive: Directive, graphQLDirective: GraphQLDirective): List = - graphQLDirective.arguments.filter { it.hasSetDefaultValue() && directive.getArgument(it.name) == null } + private fun > B.withSdlDirectives(directives: List, location: Introspection.DirectiveLocation): B = + with(sdlDirectives) { withSdlDirectives(directives, location) } private fun getSpecifiedByUrl(directives: List): String? = (directives.find { it.name == Directives.SpecifiedByDirective.name }?.getArgument("url")?.value as? StringValue)?.value @@ -627,7 +437,7 @@ class SchemaParser internal constructor( scalarType } else { if (!allowedTypeReferences.contains(typeDefinition.name)) { - throw unexpectedTypeError(typeDefinition, expectedType) + throw typeReferenceErrors.unexpectedTypeError(typeDefinition, expectedType) } namedType(typeDefinition) } @@ -647,93 +457,10 @@ class SchemaParser internal constructor( return createInputObject(definition, inputObjects, inputObjectsInProgress).also { (inputObjects as MutableList).add(it) } } - private fun unexpectedTypeError(typeName: TypeName, expectedType: KClass<*>): SchemaError { - val isInput = expectedType == GraphQLInputType::class - val definition = unusedDefinitions.find { it.name == typeName.name } - ?: definitions.filterIsInstance>().find { it.name == typeName.name } - val outputOnlyKind = when (definition) { - is ObjectTypeDefinition -> "an object type" - is InterfaceTypeDefinition -> "an interface" - is UnionTypeDefinition -> "a union" - else -> null - } - - val problem = when { - definition == null -> "is not defined in the schema." - isInput && outputOnlyKind != null -> - "is $outputOnlyKind, which can't be used as an input type. Input types are input objects, enums and scalars." - !isInput && definition is InputObjectTypeDefinition -> - "is an input object type, which can't be used as an output type. Output types are object types, interfaces, unions, enums and scalars." - definition in unusedDefinitions -> - "is defined in the schema, but was not reached while scanning the resolvers and the classes they use, so it wasn't built. " + - unreachedTypeHint(definition) - else -> "can't be used as ${if (isInput) "an input" else "an output"} type." - } - val usage = findUsage(typeName)?.let { " used by $it" }.orEmpty() - return SchemaError("Type '${typeName.name}'$usage${typeName.sourceLocation.describe()} $problem") - } - - // what makes the scanner reach the type depends on its kind, see SchemaClassScanner.canIncludeUnusedType - private fun unreachedTypeHint(definition: TypeDefinition<*>): String = when (definition) { - is InterfaceTypeDefinition, is UnionTypeDefinition -> - "Return it from a resolver method or one of those classes, or set SchemaParserOptions.includeUnusedTypes." - is EnumTypeDefinition -> - "Use it in a resolver method or one of those classes, or set SchemaParserOptions.includeUnusedTypes and pass its enum class in the parser's dictionary." - is InputObjectTypeDefinition -> - "Use it in an argument of a resolver method, directly or through another input type. SchemaParserOptions.includeUnusedTypes doesn't include input types." - is ScalarTypeDefinition -> - "Use it in a resolver method or one of those classes. SchemaParserOptions.includeUnusedTypes doesn't include scalars." - else -> - "Return it from a resolver method or one of those classes, or set SchemaParserOptions.includeUnusedTypes and pass a class for it in the parser's dictionary." - } - - // determineType only gets the type reference, so the definition using it is looked up for the error message - private fun findUsage(typeName: TypeName): String? = (definitions + typeExtensions.values.flatten()).asSequence().flatMap { definition -> - when (definition) { - is ObjectTypeDefinition -> fieldUsages(definition.name, definition.fieldDefinitions) - is InterfaceTypeDefinition -> fieldUsages(definition.name, definition.fieldDefinitions) - is InputObjectTypeDefinition -> definition.inputValueDefinitions.map { "input field '${definition.name}.${it.name}'" to it.type } - is DirectiveDefinition -> definition.inputValueDefinitions.map { "argument '${it.name}' of directive '@${definition.name}'" to it.type } - else -> emptyList() - } - }.find { (_, type) -> type.unwrap() === typeName }?.first - - private fun fieldUsages(typeName: String, fields: List): List>> = fields.flatMap { field -> - listOf("field '$typeName.${field.name}'" to field.type) + - field.inputValueDefinitions.map { "argument '${it.name}' of field '$typeName.${field.name}'" to it.type } - } - - /** - * Returns an optional [String] describing a deprecated field/enum. - * If a deprecation directive was defined using the @deprecated directive, - * then a String containing either the contents of the 'reason' argument, if present, or a default - * message defined in [DEFAULT_DEPRECATION_MESSAGE] will be returned. Otherwise, `null` will be returned - * indicating no deprecation directive was found within the directives list. - */ - private fun getDeprecated(directives: List): String? = - directives.find { it.name == "deprecated" }?.let { directive -> - (directive.arguments.find { it.name == "reason" }?.value as? StringValue)?.value - ?: DEFAULT_DEPRECATION_MESSAGE - } } class SchemaError(message: String, cause: Throwable? = null) : RuntimeException(message, cause) val GRAPHQL_SCALARS = ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS.associateBy { it.name } -// Built from the individual constants rather than Directives.BUILT_IN_DIRECTIVES_MAP (added in graphql-java 26) -// so that the library keeps working with graphql-java 25. TODO replace with Directives.BUILT_IN_DIRECTIVES_MAP once we deploy a major version -private val BUILT_IN_DIRECTIVES = listOf( - Directives.IncludeDirective, - Directives.SkipDirective, - Directives.DeprecatedDirective, - Directives.SpecifiedByDirective, - Directives.OneOfDirective, - Directives.DeferDirective, - Directives.ExperimentalDisableErrorPropagationDirective -).associateBy { it.name } - const val DEFAULT_DEPRECATION_MESSAGE = "No longer supported" - -private fun SourceLocation?.describe(): String = - this?.let { " at line $line, column $column" + sourceName?.let { name -> " in $name" }.orEmpty() }.orEmpty() diff --git a/src/main/kotlin/graphql/kickstart/tools/SdlDirectives.kt b/src/main/kotlin/graphql/kickstart/tools/SdlDirectives.kt new file mode 100644 index 00000000..6f0f6d7f --- /dev/null +++ b/src/main/kotlin/graphql/kickstart/tools/SdlDirectives.kt @@ -0,0 +1,252 @@ +package graphql.kickstart.tools + +import graphql.Directives +import graphql.Scalars +import graphql.introspection.Introspection +import graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION +import graphql.introspection.Introspection.DirectiveLocation.SCHEMA +import graphql.kickstart.tools.util.describe +import graphql.kickstart.tools.util.getDocumentation +import graphql.language.* +import graphql.schema.* +import graphql.schema.idl.RuntimeWiring +import org.slf4j.Logger + +internal class SdlDirectives( + private val directiveDefinitions: List, + private val options: SchemaParserOptions, + private val runtimeWiring: RuntimeWiring, + private val log: Logger, + private val inputType: (Type<*>) -> GraphQLInputType +) { + // by name, in the order they're declared + private lateinit var schemaDirectives: MutableMap + + val definitions: Collection get() = schemaDirectives.values + + init { + createDirectives() + } + + fun directivesOf(definition: TypeDefinition<*>, extensions: List>): List { + val applied = definition.directives.mapTo(mutableSetOf()) { it.name } + return definition.directives + extensions.flatMap { extension -> + val (repeated, kept) = extension.directives.partition { it.name in applied && isNonRepeatableByFirstDeclaration(it.name) } + repeated.forEach { log.warn("Directive @${it.name} isn't repeatable but an extension of type '${definition.name}' applies it again, only the first one is kept.") } + applied += kept.map { it.name } + kept + } + } + + private fun isNonRepeatableByFirstDeclaration(directiveName: String): Boolean = + directiveDefinitions.find { it.name == directiveName }?.let { !it.isRepeatable } ?: BUILT_IN_DIRECTIVES.containsKey(directiveName) + + private fun createDirectives() { + schemaDirectives = directiveDefinitions.associateTo(linkedMapOf()) { it.name to createDirective(it) } + // because the arguments can have directives too, we attach them only after the directives themselves are created + schemaDirectives.replaceAll { _, d -> + val arguments = d.arguments.map { a -> a.transform { + it.withSdlDirectives(a.definition!!.directives, ARGUMENT_DEFINITION) + } } + d.transform { it.replaceArguments(arguments) } + } + } + + private fun createDirective(definition: DirectiveDefinition): GraphQLDirective { + val locations = definition.directiveLocations.map { Introspection.DirectiveLocation.valueOf(it.name) }.toTypedArray() + + return GraphQLDirective.newDirective() + .name(definition.name) + .description(getDocumentation(definition, options)) + .definition(definition) + .comparatorRegistry(runtimeWiring.comparatorRegistry) + .validLocations(*locations) + .repeatable(definition.isRepeatable) + .apply { + definition.inputValueDefinitions.forEach { argumentDefinition -> + argument(createDirectiveArgument(argumentDefinition)) + } + } + .build() + } + + private fun createDirectiveArgument(definition: InputValueDefinition): GraphQLArgument { + return GraphQLArgument.newArgument() + .name(definition.name) + .definition(definition) + .description(getDocumentation(definition, options)) + .type(inputType(definition.type)) + .apply { getDeprecated(definition.directives)?.let { deprecate(it) } } + .apply { definition.defaultValue?.let { defaultValueLiteral(it) } } + .build() + } + + @Suppress("DEPRECATION") + fun > B.withSdlDirectives(directives: List, location: Introspection.DirectiveLocation): B { + if (directives.isEmpty()) return this + val resolved = resolveDirectives(directives, location) + val applied = appliedDirectives(resolved) + val legacy = legacyDirectives(resolved, location) + return withAppliedDirectives(*applied).withDirectives(*legacy) + } + + fun schemaAppliedDirectives(directives: List): List = + appliedDirectives(resolveDirectives(directives, SCHEMA)).toList() + + private fun resolveDirectives(directives: List, location: Introspection.DirectiveLocation): List { + // TODO throw a SchemaError instead in 15.0.0 (#884), like graphql-java does + directiveProblems(directives, location).forEach { log.warn(it) } + return directives.map { directive -> + val definition = findDirective(directive.name) + when { + definition != null -> ResolvedDirective(directive, declaredArguments(directive, definition), declared = true) + options.allowUndeclaredDirectives -> ResolvedDirective(directive, undeclaredArguments(directive), declared = false) + else -> throw SchemaError("Found applied directive ${directive.name} without corresponding directive definition.") + } + } + } + + // missing arguments get their defaults like in graphql-java, which gives a bare @deprecated the "reason" SchemaPrinter needs + private fun declaredArguments(directive: Directive, definition: GraphQLDirective): List { + val supplied = directive.arguments.map { arg -> + val argument = definition.getArgument(arg.name) + ?: throw SchemaError("Found an unexpected directive argument ${directive.name}#${arg.name} .") + ResolvedArgument(arg.name, argument.type, InputValueWithState.newLiteralValue(arg.value), legacyDescription = null) + } + val defaulted = missingArgumentsWithDefault(directive, definition).map { + ResolvedArgument(it.name, it.type, it.argumentDefaultValue, legacyDescription = it.description) + } + return supplied + defaulted + } + + private fun undeclaredArguments(directive: Directive): List = + directive.arguments.map { arg -> + ResolvedArgument(arg.name, guessDirectiveArgumentType(directive, arg.name, arg.value), InputValueWithState.newLiteralValue(arg.value), legacyDescription = null) + } + + private fun directiveProblems(directives: List, location: Introspection.DirectiveLocation): List = + directives.groupBy { it.name }.flatMap { (name, applied) -> + // undeclared directives have no definition to check against + val definition = findDirective(name) ?: return@flatMap emptyList() + buildList { + if (location !in definition.validLocations()) { + add("Directive @$name${applied.first().sourceLocation.describe()} is applied to $location, but it's declared on ${definition.validLocations().joinToString(" | ")}.") + } + if (definition.isNonRepeatable && applied.size > 1) { + add("Directive @$name${applied[1].sourceLocation.describe()} isn't repeatable but is applied more than once.") + } + applied.forEach { directive -> + definition.arguments + .filter { it.type is GraphQLNonNull && !it.hasSetDefaultValue() && directive.getArgument(it.name) == null } + .forEach { add("Directive @$name${directive.sourceLocation.describe()} is missing its required argument '${it.name}'.") } + } + } + } + + private fun findDirective(name: String): GraphQLDirective? = schemaDirectives[name] ?: BUILT_IN_DIRECTIVES[name] + + // there's no directive definition to look the type up in, so it's guessed from the value + private fun guessDirectiveArgumentType(directive: Directive, argumentName: String, value: Value<*>): GraphQLInputType { + return when (value) { + is StringValue, is NullValue -> Scalars.GraphQLString + is IntValue -> Scalars.GraphQLInt + is FloatValue -> Scalars.GraphQLFloat + is BooleanValue -> Scalars.GraphQLBoolean + is ArrayValue -> GraphQLList(value.values.firstOrNull()?.let { guessDirectiveArgumentType(directive, argumentName, it) } ?: Scalars.GraphQLString) + else -> throw SchemaError("Can't guess the type of argument ${directive.name}#$argumentName of undeclared directive ${directive.name}, please declare the directive.") + } + } + + // graphql-java's applied directives and their arguments have no description + private fun appliedDirectives(directives: List): Array = + directives.map { directive -> + GraphQLAppliedDirective.newDirective() + .name(directive.usage.name) + .definition(directive.usage) + .comparatorRegistry(runtimeWiring.comparatorRegistry) + .apply { + directive.arguments.forEach { arg -> + argument(GraphQLAppliedDirectiveArgument.newArgument() + .name(arg.name) + .type(arg.type) + .inputValueWithState(arg.value) + .build() + ) + } + } + .build() + }.toTypedArray() + + // TODO remove this once directives are fully replaced with applied directives + @Suppress("DEPRECATION") // legacy directive argument values + private fun legacyDirectives(directives: List, location: Introspection.DirectiveLocation): Array { + val seen = mutableSetOf() + return directives.mapNotNull { directive -> + // graphql-java rejects legacy directives without a definition, so undeclared ones are only kept as applied directives + if (!directive.declared) return@mapNotNull null + val name = directive.usage.name + val nonRepeatable = isNonRepeatableByFirstDeclaration(name) + // graphql-java rejects a non-repeatable legacy directive that is applied more than once + if (!seen.add(name) && nonRepeatable) return@mapNotNull null + GraphQLDirective.newDirective() + .name(name) + .description(getDocumentation(directive.usage, options)) + .comparatorRegistry(runtimeWiring.comparatorRegistry) + .validLocation(location) + .repeatable(!nonRepeatable) + .apply { + directive.arguments.forEach { arg -> + argument(GraphQLArgument.newArgument() + .name(arg.name) + .type(arg.type) + .description(arg.legacyDescription) + // there's no InputValueWithState setter. only a default can be programmatic, like @deprecated's reason + .apply { if (arg.value.isLiteral) valueLiteral(arg.value.value as Value<*>) else valueProgrammatic(arg.value.value) } + .build()) + } + } + .build() + }.toTypedArray() + } + + private class ResolvedDirective( + val usage: Directive, + val arguments: List, + val declared: Boolean + ) + + private class ResolvedArgument( + val name: String, + val type: GraphQLInputType, + val value: InputValueWithState, + val legacyDescription: String? + ) + + private fun missingArgumentsWithDefault(directive: Directive, graphQLDirective: GraphQLDirective): List = + graphQLDirective.arguments.filter { it.hasSetDefaultValue() && directive.getArgument(it.name) == null } + + /** + * Returns an optional [String] describing a deprecated field/enum. + * If a deprecation directive was defined using the @deprecated directive, + * then a String containing either the contents of the 'reason' argument, if present, or a default + * message defined in [DEFAULT_DEPRECATION_MESSAGE] will be returned. Otherwise, `null` will be returned + * indicating no deprecation directive was found within the directives list. + */ + fun getDeprecated(directives: List): String? = + directives.find { it.name == "deprecated" }?.let { directive -> + (directive.arguments.find { it.name == "reason" }?.value as? StringValue)?.value + ?: DEFAULT_DEPRECATION_MESSAGE + } +} + +// Built from the individual constants rather than Directives.BUILT_IN_DIRECTIVES_MAP (added in graphql-java 26) +// so that the library keeps working with graphql-java 25. TODO replace with Directives.BUILT_IN_DIRECTIVES_MAP once we deploy a major version +private val BUILT_IN_DIRECTIVES = listOf( + Directives.IncludeDirective, + Directives.SkipDirective, + Directives.DeprecatedDirective, + Directives.SpecifiedByDirective, + Directives.OneOfDirective, + Directives.DeferDirective, + Directives.ExperimentalDisableErrorPropagationDirective +).associateBy { it.name } diff --git a/src/main/kotlin/graphql/kickstart/tools/SealedSubclassMatcher.kt b/src/main/kotlin/graphql/kickstart/tools/SealedSubclassMatcher.kt new file mode 100644 index 00000000..671e940d --- /dev/null +++ b/src/main/kotlin/graphql/kickstart/tools/SealedSubclassMatcher.kt @@ -0,0 +1,74 @@ +package graphql.kickstart.tools + +import graphql.kickstart.tools.ScanDictionary.SealedSubclassReference +import graphql.kickstart.tools.util.JavaType +import graphql.kickstart.tools.util.unwrap +import graphql.kickstart.tools.util.withExtensions +import graphql.language.InterfaceTypeDefinition +import graphql.language.ObjectTypeDefinition +import graphql.language.TypeDefinition +import graphql.language.TypeName +import graphql.language.UnionTypeDefinition +import org.apache.commons.lang3.reflect.TypeUtils +import java.lang.reflect.ParameterizedType + +internal class SealedSubclassMatcher( + private val dictionary: Map, ScanDictionary.EntryView>, + private val typeExtensions: TypeExtensions, +) { + fun sealedSubclassHint(type: TypeDefinition<*>): String { + if (type !is ObjectTypeDefinition) { + return "" + } + + val sealedTypes = sealedTypesOf(type) + if (sealedTypes.isEmpty()) { + return "" + } + + val sealedTypeNames = sealedTypes.joinToString(" or ") { it.unwrap().name } + val matches = sealedSubclassMatches(type) + return if (matches.isEmpty()) { + " No subclass of sealed type $sealedTypeNames is named '${type.name}'." + } else { + " More than one subclass of sealed type $sealedTypeNames is named '${type.name}': ${matches.joinToString { it.javaType.unwrap().name }}." + } + } + + fun sealedSubclassMatches(type: ObjectTypeDefinition): List = + sealedTypesOf(type) + .flatMap { sealedType -> + sealedSubclasses(sealedType.unwrap()) + .filter { it.simpleName == type.name } + .map { TypeClassMatcher.ValidMatch(type, withSealedTypeArguments(it, sealedType), SealedSubclassReference(sealedType)) } + } + .distinctBy { it.javaType.unwrap() } + + private fun sealedTypesOf(type: ObjectTypeDefinition): List = + dictionary + .filterKeys { definition -> + when (definition) { + is UnionTypeDefinition -> definition.withExtensions(typeExtensions).flatMap { it.memberTypes }.filterIsInstance().any { it.name == type.name } + is InterfaceTypeDefinition -> type.withExtensions(typeExtensions).flatMap { it.implements }.filterIsInstance().any { it.name == definition.name } + else -> false + } + } + .values.mapNotNull { it.javaType } + .distinct() + .filter { it.unwrap().kotlin.isSealed } + + // kotlin-reflect also returns the permitted subclasses of Java sealed classes + private fun sealedSubclasses(clazz: Class<*>): List> = + clazz.kotlin.sealedSubclasses.flatMap { listOf(it.java) + sealedSubclasses(it.java) } + + private fun withSealedTypeArguments(subclass: Class<*>, sealedType: JavaType): JavaType { + if (sealedType !is ParameterizedType || subclass.typeParameters.isEmpty()) { + return subclass + } + + return TypeUtils.determineTypeArguments(subclass, sealedType) + ?.takeIf { it.keys.containsAll(subclass.typeParameters.asList()) } + ?.let { TypeUtils.parameterize(subclass, it) } + ?: subclass + } +} diff --git a/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt b/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt index 1089e5db..a4244214 100644 --- a/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt +++ b/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt @@ -95,7 +95,7 @@ internal class TypeClassMatcher(private val definitionsByName: Map, val javaType: JavaType, val reference: SchemaClassScanner.Reference) : Match + internal data class ValidMatch(val type: TypeDefinition<*>, val javaType: JavaType, val reference: ScanDictionary.Reference) : Match internal enum class Location(val prettyName: String) { RETURN_TYPE("return type"), @@ -106,14 +106,14 @@ internal class TypeClassMatcher(private val definitionsByName: Map>, + private val unusedDefinitions: Set>, + private val typeExtensions: TypeExtensions, +) { + fun unexpectedTypeError(typeName: TypeName, expectedType: KClass<*>): SchemaError { + val isInput = expectedType == GraphQLInputType::class + val definition = unusedDefinitions.find { it.name == typeName.name } + ?: definitions.filterIsInstance>().find { it.name == typeName.name } + val outputOnlyKind = when (definition) { + is ObjectTypeDefinition -> "an object type" + is InterfaceTypeDefinition -> "an interface" + is UnionTypeDefinition -> "a union" + else -> null + } + + val problem = when { + definition == null -> "is not defined in the schema." + isInput && outputOnlyKind != null -> + "is $outputOnlyKind, which can't be used as an input type. Input types are input objects, enums and scalars." + !isInput && definition is InputObjectTypeDefinition -> + "is an input object type, which can't be used as an output type. Output types are object types, interfaces, unions, enums and scalars." + definition in unusedDefinitions -> + "is defined in the schema, but was not reached while scanning the resolvers and the classes they use, so it wasn't built. " + + unreachedTypeHint(definition) + else -> "can't be used as ${if (isInput) "an input" else "an output"} type." + } + val usage = findUsage(typeName)?.let { " used by $it" }.orEmpty() + return SchemaError("Type '${typeName.name}'$usage${typeName.sourceLocation.describe()} $problem") + } + + // what makes the scanner reach the type depends on its kind, see SchemaClassScanner.canIncludeUnusedType + private fun unreachedTypeHint(definition: TypeDefinition<*>): String = when (definition) { + is InterfaceTypeDefinition, is UnionTypeDefinition -> + "Return it from a resolver method or one of those classes, or set SchemaParserOptions.includeUnusedTypes." + is EnumTypeDefinition -> + "Use it in a resolver method or one of those classes, or set SchemaParserOptions.includeUnusedTypes and pass its enum class in the parser's dictionary." + is InputObjectTypeDefinition -> + "Use it in an argument of a resolver method, directly or through another input type. SchemaParserOptions.includeUnusedTypes doesn't include input types." + is ScalarTypeDefinition -> + "Use it in a resolver method or one of those classes. SchemaParserOptions.includeUnusedTypes doesn't include scalars." + else -> + "Return it from a resolver method or one of those classes, or set SchemaParserOptions.includeUnusedTypes and pass a class for it in the parser's dictionary." + } + + // determineType only gets the type reference, so the definition using it is looked up for the error message + private fun findUsage(typeName: TypeName): String? = (definitions + typeExtensions.values.flatten()).asSequence().flatMap { definition -> + when (definition) { + is ObjectTypeDefinition -> fieldUsages(definition.name, definition.fieldDefinitions) + is InterfaceTypeDefinition -> fieldUsages(definition.name, definition.fieldDefinitions) + is InputObjectTypeDefinition -> definition.inputValueDefinitions.map { "input field '${definition.name}.${it.name}'" to it.type } + is DirectiveDefinition -> definition.inputValueDefinitions.map { "argument '${it.name}' of directive '@${definition.name}'" to it.type } + else -> emptyList() + } + }.find { (_, type) -> type.unwrap() === typeName }?.first + + private fun fieldUsages(typeName: String, fields: List): List>> = fields.flatMap { field -> + listOf("field '$typeName.${field.name}'" to field.type) + + field.inputValueDefinitions.map { "argument '${it.name}' of field '$typeName.${field.name}'" to it.type } + } +} diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/MapFieldResolver.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/MapFieldResolver.kt index 0eea1c1d..79b5bb4f 100644 --- a/src/main/kotlin/graphql/kickstart/tools/resolver/MapFieldResolver.kt +++ b/src/main/kotlin/graphql/kickstart/tools/resolver/MapFieldResolver.kt @@ -1,7 +1,7 @@ package graphql.kickstart.tools.resolver import com.fasterxml.classmate.TypeResolver -import graphql.kickstart.tools.SchemaClassScanner +import graphql.kickstart.tools.ScanDictionary import graphql.kickstart.tools.SchemaParserOptions import graphql.kickstart.tools.TypeClassMatcher import graphql.kickstart.tools.util.JavaType @@ -44,7 +44,7 @@ internal class MapFieldResolver( } override fun scanForMatches(): List { - return listOf(TypeClassMatcher.PotentialMatch.returnValue(field.type, mapGenericValue, typeResolver, SchemaClassScanner.FieldTypeReference(field.name))) + return listOf(TypeClassMatcher.PotentialMatch.returnValue(field.type, mapGenericValue, typeResolver, ScanDictionary.FieldTypeReference(field.name))) } override fun toString() = "MapFieldResolver{key=${field.name}}" diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt index b863b882..a1a7cf41 100644 --- a/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt +++ b/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt @@ -160,10 +160,10 @@ internal class MethodFieldResolver( } override fun scanForMatches(): List { - val returnValueMatch = TypeClassMatcher.PotentialMatch.returnValue(field.type, getResolvedReturnType(), typeResolver, SchemaClassScanner.ReturnValueReference(method)) + val returnValueMatch = TypeClassMatcher.PotentialMatch.returnValue(field.type, getResolvedReturnType(), typeResolver, ScanDictionary.ReturnValueReference(method)) return field.inputValueDefinitions.mapIndexed { i, inputDefinition -> - TypeClassMatcher.PotentialMatch.parameterType(inputDefinition.type, getMethodParameterType(i)!!, typeResolver, SchemaClassScanner.MethodParameterReference(method, i)) + TypeClassMatcher.PotentialMatch.parameterType(inputDefinition.type, getMethodParameterType(i)!!, typeResolver, ScanDictionary.MethodParameterReference(method, i)) } + listOf(returnValueMatch) } diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/PropertyFieldResolver.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/PropertyFieldResolver.kt index 818186e8..1e6cb062 100644 --- a/src/main/kotlin/graphql/kickstart/tools/resolver/PropertyFieldResolver.kt +++ b/src/main/kotlin/graphql/kickstart/tools/resolver/PropertyFieldResolver.kt @@ -1,6 +1,6 @@ package graphql.kickstart.tools.resolver -import graphql.kickstart.tools.SchemaClassScanner +import graphql.kickstart.tools.ScanDictionary import graphql.kickstart.tools.SchemaParserOptions import graphql.kickstart.tools.TypeClassMatcher import graphql.language.FieldDefinition @@ -31,7 +31,7 @@ internal class PropertyFieldResolver( field.type, property.genericType, typeResolver, - SchemaClassScanner.FieldTypeReference(property.toString()) + ScanDictionary.FieldTypeReference(property.toString()) ) ) } diff --git a/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt b/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt index 91755b5f..bcd52629 100644 --- a/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt +++ b/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt @@ -116,3 +116,6 @@ internal fun getDocumentation(node: AbstractNode<*>, options: SchemaParserOption } internal fun String.snakeToCamelCase(): String = split("_").joinToString(separator = "") { it.replaceFirstChar(Char::titlecase) } + +internal fun SourceLocation?.describe(): String = + this?.let { " at line $line, column $column" + sourceName?.let { name -> " in $name" }.orEmpty() }.orEmpty() diff --git a/src/test/kotlin/graphql/kickstart/tools/NestedGenericWrapperTest.kt b/src/test/kotlin/graphql/kickstart/tools/NestedGenericWrapperTest.kt index 2a935dfb..0684b81f 100644 --- a/src/test/kotlin/graphql/kickstart/tools/NestedGenericWrapperTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/NestedGenericWrapperTest.kt @@ -181,7 +181,7 @@ class NestedGenericWrapperTest { val accepted = javaTypes.flatMap { (shape, javaType) -> graphQLTypes.map { Triple(shape, javaType, it) } }.filter { (_, javaType, graphQLType) -> val match = try { - matcher.match(TypeClassMatcher.PotentialMatch.returnValue(graphQLType, typeResolver.resolve(javaType), typeResolver, SchemaClassScanner.ReturnValueReference(method))) + matcher.match(TypeClassMatcher.PotentialMatch.returnValue(graphQLType, typeResolver.resolve(javaType), typeResolver, ScanDictionary.ReturnValueReference(method))) } catch (e: SchemaClassScannerError) { null }