@@ -8,15 +8,17 @@ import graphql.language.*
88import graphql.schema.GraphQLScalarType
99import graphql.schema.idl.ScalarInfo
1010import org.apache.commons.lang3.ClassUtils
11+ import org.apache.commons.lang3.reflect.TypeUtils
1112import org.slf4j.LoggerFactory
1213import java.lang.reflect.Member
1314import java.lang.reflect.Method
15+ import java.lang.reflect.ParameterizedType
1416
1517/* *
1618 * @author Andrew Potter
1719 */
1820internal class SchemaClassScanner (
19- initialDictionary : BiMap <String , Class <* >>,
21+ initialDictionary : Map <String , Class <* >>,
2022 allDefinitions : List <Definition <* >>,
2123 resolvers : List <GraphQLResolver <* >>,
2224 private val scalars : CustomScalarMap ,
@@ -59,6 +61,17 @@ internal class SchemaClassScanner(
5961 }
6062 }
6163
64+ // Input object types never go into the type => class bimap that interface and union type resolution use, so several of
65+ // them can share a class, e.g. ObjectNode
66+ initialDictionary
67+ .filterKeys { definitionsByName[it] !is InputObjectTypeDefinition }
68+ .entries.groupBy({ it.value }, { it.key })
69+ .forEach { (clazz, names) ->
70+ if (names.size > 1 ) {
71+ throw SchemaClassScannerError (" Class in supplied dictionary '${clazz.name} ' specified type names ${names.joinToString { " '$it '" }} , but only input object types can share a class!" )
72+ }
73+ }
74+
6275 if (options.allowUnimplementedResolvers) {
6376 log.warn(" Option 'allowUnimplementedResolvers' should only be set to true during development, as it can cause schema errors to be moved to query time instead of schema creation time. Make sure this is turned off in production." )
6477 }
@@ -348,14 +361,103 @@ internal class SchemaClassScanner(
348361 }
349362
350363 private fun handleDictionaryTypes (types : List <TypeDefinition <* >>, failureMessage : (TypeDefinition <* >) -> String ) {
364+ val typesWithoutClass = mutableListOf<TypeDefinition <* >>()
351365 types.forEach { type ->
352366 val dictionaryContainsType = dictionary.filter { it.key.name == type.name }.isNotEmpty()
353367 if (! unvalidatedTypes.contains(type) && ! dictionaryContainsType) {
354368 val initialEntry = initialDictionary[type.name]
355- ? : throw SchemaClassScannerError (failureMessage(type))
356- handleFoundType(type, initialEntry.get(), DictionaryReference ())
369+ if (initialEntry != null ) {
370+ handleFoundType(type, initialEntry.get(), DictionaryReference ())
371+ } else {
372+ typesWithoutClass.add(type)
373+ }
374+ }
375+ }
376+
377+ // Scanning a sealed subclass can find the classes of the other types, so only fail when no type has exactly one sealed
378+ // subclass named after it. Types still without a class fail on the next pass.
379+ val sealedMatches = typesWithoutClass.filterIsInstance<ObjectTypeDefinition >().mapNotNull { sealedSubclassMatches(it).singleOrNull() }
380+ if (sealedMatches.isEmpty()) {
381+ typesWithoutClass.firstOrNull()?.let { throw SchemaClassScannerError (failureMessage(it) + sealedSubclassHint(it)) }
382+ }
383+
384+ sealedMatches.forEach { handleFoundType(it) }
385+ }
386+
387+ /* *
388+ * Explains why no sealed subclass was used for the type, if any of its unions or interfaces has a sealed class.
389+ */
390+ private fun sealedSubclassHint (type : TypeDefinition <* >): String {
391+ if (type !is ObjectTypeDefinition ) {
392+ return " "
393+ }
394+
395+ val sealedTypes = sealedTypesOf(type)
396+ if (sealedTypes.isEmpty()) {
397+ return " "
398+ }
399+
400+ val sealedTypeNames = sealedTypes.joinToString(" or " ) { it.unwrap().name }
401+ val matches = sealedSubclassMatches(type)
402+ return if (matches.isEmpty()) {
403+ " No subclass of sealed type $sealedTypeNames is named '${type.name} '."
404+ } else {
405+ " More than one subclass of sealed type $sealedTypeNames is named '${type.name} ': ${matches.joinToString { it.javaType.unwrap().name }} ."
406+ }
407+ }
408+
409+ /* *
410+ * A member of a union or an implementor of an interface may never be returned directly, e.g. `NormalPet` when only
411+ * `pets(): List<Pet>` returns `union Pet`. If the class found for the union or interface is sealed, its subclass named
412+ * after the object type can be used instead.
413+ */
414+ private fun sealedSubclassMatches (type : ObjectTypeDefinition ): List <TypeClassMatcher .ValidMatch > =
415+ sealedTypesOf(type)
416+ .flatMap { sealedType ->
417+ sealedSubclasses(sealedType.unwrap())
418+ .filter { it.simpleName == type.name }
419+ .map { TypeClassMatcher .ValidMatch (type, parameterize(it, sealedType), SealedSubclassReference (sealedType)) }
420+ }
421+ // a subclass is found twice when it's under two of these sealed types, or extends both a sealed type and one of its
422+ // sealed subclasses
423+ .distinctBy { it.javaType.unwrap() }
424+
425+ /* *
426+ * The sealed classes found for the unions the type is a member of and the interfaces it implements.
427+ */
428+ private fun sealedTypesOf (type : ObjectTypeDefinition ): List <JavaType > =
429+ dictionary
430+ .filterKeys { definition ->
431+ when (definition) {
432+ is UnionTypeDefinition -> definition.memberTypes.filterIsInstance<TypeName >().any { it.name == type.name }
433+ is InterfaceTypeDefinition -> type.implements.filterIsInstance<TypeName >().any { it.name == definition.name }
434+ else -> false
435+ }
357436 }
437+ .values.mapNotNull { it.javaType }
438+ .distinct()
439+ .filter { it.unwrap().kotlin.isSealed }
440+
441+ /* *
442+ * All subclasses of a sealed class, including those of its sealed subclasses. kotlin-reflect also returns the permitted
443+ * subclasses of Java sealed classes.
444+ */
445+ private fun sealedSubclasses (clazz : Class <* >): List <Class <* >> =
446+ clazz.kotlin.sealedSubclasses.flatMap { listOf (it.java) + sealedSubclasses(it.java) }
447+
448+ /* *
449+ * Passes the type arguments of a parameterized sealed class on to its generic subclass, e.g. `Success<Pet>` for
450+ * `Result<Pet>`, so the types of the subclass's fields can be resolved.
451+ */
452+ private fun parameterize (subclass : Class <* >, sealedType : JavaType ): JavaType {
453+ if (sealedType !is ParameterizedType || subclass.typeParameters.isEmpty()) {
454+ return subclass
358455 }
456+
457+ return TypeUtils .determineTypeArguments(subclass, sealedType)
458+ ?.takeIf { it.keys.containsAll(subclass.typeParameters.asList()) }
459+ ?.let { TypeUtils .parameterize(subclass, it) }
460+ ? : subclass
359461 }
360462
361463 /* *
@@ -625,6 +727,10 @@ internal class SchemaClassScanner(
625727 override fun getDescription () = " interface declarations of ${type.name} "
626728 }
627729
730+ private class SealedSubclassReference (private val sealedType : JavaType ) : Reference() {
731+ override fun getDescription () = " subclass of sealed type ${sealedType.typeName} "
732+ }
733+
628734 private class InputObjectReference (private val type : InputValueDefinition ) : Reference() {
629735 override fun getDescription () = " input object $type "
630736 }
0 commit comments