diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index f73b12f7..3d383130 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -113,6 +113,9 @@ internal class SchemaClassScanner( handleDictionaryTypes(getAllObjectTypesImplementingDiscoveredInterfaces()) { "Object type '${it.name}' implements a known interface, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } if (scanQueue()) continue + // Discover unions listed as members of discovered unions, even when no field returns them. + if (handleNestedUnions()) continue + // Require all members of discovered unions to be discovered. handleDictionaryTypes(getAllObjectTypeMembersOfDiscoveredUnions()) { "Object type '${it.name}' is a member of a known union, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } if (scanQueue()) continue @@ -359,6 +362,22 @@ internal class SchemaClassScanner( }.flatten().distinct() } + /** + * Unions can list other unions as members. Returns whether any new unions were found. + */ + private fun handleNestedUnions(): Boolean { + val newUnions = dictionary.keys.filterIsInstance().flatMap { union -> + union.withExtensions(typeExtensions).flatMap { it.memberTypes }.filterIsInstance() + .map { definitionsByName[it.name] } + .filterIsInstance() + .filter { !dictionary.containsKey(it) } + .map { it to UnionMemberReference(union) } + } + + newUnions.forEach { (type, reference) -> handleFoundType(type, null, reference) } + return newUnions.isNotEmpty() + } + /** * Implementations can narrow an interface field to a member of its union or an implementor of its interface, so such a * union or interface may not be reachable from any object field. Returns whether any new types were found. @@ -755,6 +774,10 @@ internal class SchemaClassScanner( 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}" } diff --git a/src/test/kotlin/graphql/kickstart/tools/TypeExtensionTest.kt b/src/test/kotlin/graphql/kickstart/tools/TypeExtensionTest.kt index 0e86778d..e98dc9a7 100644 --- a/src/test/kotlin/graphql/kickstart/tools/TypeExtensionTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/TypeExtensionTest.kt @@ -92,17 +92,17 @@ class TypeExtensionTest { } @Test - fun `union extensions can add nested unions`() { + fun `union extensions can add nested unions that no field returns`() { val schema = parser( """ type Query { fooBar: FooBar - barBaz: BarBaz } union FooBar = Foo extend union FooBar = BarBaz - union BarBaz = Bar | Baz + union BarBaz = Bar | BazUnion + union BazUnion = Baz $fooBarBaz """) { @@ -111,6 +111,7 @@ class TypeExtensionTest { .assertEveryFieldResolves() assertEquals((schema.getType("FooBar") as GraphQLUnionType).types.map { it.name }, listOf("Foo", "Bar", "Baz")) + assertEquals((schema.getType("BarBaz") as GraphQLUnionType).types.map { it.name }, listOf("Bar", "Baz")) } @Test @@ -475,7 +476,6 @@ class TypeExtensionTest { class Query : GraphQLQueryResolver { fun fooBar(): FooBar = Baz("baz") - fun barBaz(): FooBar? = null fun color(): Color = Color.GREEN fun node(): Node = Item("1", "item") fun search(filter: Filter?): String? = null