Repository: graphql-java-kickstart/graphql-java-tools (kotlin)

Issue:
Expected object type [...] to exist for union
<!-- ⚠️ Please ensure you are reporting the bug in the correct repository! -->

## Description

Suppose we have a union U = Foo | Bar and GraphQL type Bar is mapped to the Java interface Bar. In this case the concrete value for type Bar must be a class that implements Bar, i.e.:
```java
public interface Bar { ... }
public class BarImpl implements Bar { ... }

// and then a resolver method somewhere (GraphQL signature is `fooOrBar: U!`):
public Object getFooOrBar() { return new BarImpl(...); }
```
The UnionTypeResolver supports this scenario due to [PR 140](https://github.com/graphql-java-kickstart/graphql-java-tools/pull/140), which looks at the direct interfaces of the value.

However, if the type hierarchy is a bit more complex it will fail. Suppose we have an additional intermediate interface:
```java
public interface Bar { ... }
public interface BarEntity { ... }
public class BarEntityImpl implements BarEntity { ... }

// and then a resolver method somewhere (GraphQL signature is `fooOrBar: U!`)
public Object getFooOrBar() { return new BarEntityImpl(...); }
```
In this case the resolver will fail to resolve the type, since GraphQL type Bar is mapped to interface Bar, and that's not a direct superinterface of BarEntityImpl.

### Expected behavior

I would expect class BarEntityImpl to succesfully resolve to GraphQL type Bar, since it implements (indirectly) interface Bar.

### Actual behavior

Resolving fails with error "Expected object type with name 'BarEntityImpl' to exist for union 'U', but it doesn't!".

### Steps to reproduce the bug

Pull request #559 reproduces this issue.

Pull request:
<!-- Uncomment one of the following lines as necessary. -->
Fixes #558
<!-- Resolves #<ISSUE_NUMBER> -->

## Checklist
<!-- Change [ ] to [x] to indicate you acknowledge the check. -->
- [x] Pull requests follows the [contribution guide](https://github.com/graphql-java-kickstart/graphql-java-tools/wiki/Contribution-guide)
- [x] New or modified functionality is covered by tests

## Description
DictionaryTypeResolver now considers the full type hierarchy -- superinterfaces in addition to superclasses.

Patch:
diff --git a/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt b/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt
index 7851c134..e5f0a901 100644
--- a/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt
@@ -16,22 +16,15 @@ internal abstract class DictionaryTypeResolver(
     private val dictionary: BiMap<JavaType, TypeDefinition<*>>,
     private val types: Map<String, GraphQLObjectType>
 ) : TypeResolver {
-    private fun <T> getTypeName(clazz: Class<T>): String? {
-        val name = dictionary[clazz]?.name
-
-        if (name == null && clazz.superclass != null) {
-            return getTypeName(clazz.superclass)
-        }
-
-        return name
+    private fun <T> getTypeDefinition(clazz: Class<T>): TypeDefinition<*>? {
+        return dictionary[clazz]
+            ?: (if (clazz.superclass == null) null else getTypeDefinition(clazz.superclass))
+            ?: clazz.interfaces.mapNotNull { getTypeDefinition(it) }.firstOrNull()
     }
 
     override fun getType(env: TypeResolutionEnvironment): GraphQLObjectType? {
         val clazz = env.getObject<Any>().javaClass
-        val name = clazz.interfaces.fold(getTypeName(clazz), { name, interfaceClazz ->
-            name ?: getTypeName(interfaceClazz)
-        }) ?: clazz.simpleName
-
+        val name = getTypeDefinition(clazz)?.name ?: clazz.simpleName
         return types[name] ?: throw TypeResolverError(getError(name))
     }
 


Test patch:
diff --git a/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt b/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt
index 8047a1ae..e77d5ed6 100644
--- a/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt
+++ b/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt
@@ -22,6 +22,7 @@ fun createSchema() = SchemaParser.newParser()
     .dictionary("ThirdItem", ThirdItem::class)
     .dictionary("ComplexMapItem", ComplexMapItem::class)
     .dictionary("NestedComplexMapItem", NestedComplexMapItem::class)
+    .dictionary("NoDogError", NoDogError::class)
     .build()
     .makeExecutableSchema()
 
@@ -83,6 +84,9 @@ type Query {
     arrayItems: [Item!]!
     
     throwsIllegalArgumentException: String
+    
+    allDogs: [Dog!]!
+    findSuitableDog(preferredColor: String!, minimumFluffiness: Int!): FindDogResult!
 }
 
 type ExtendedType {
@@ -216,6 +220,18 @@ type Tag {
 type ItemWithGenericProperties {
     keys: [String!]!
 }
+
+type Dog {
+    name: String!
+    color: String!
+    fluffiness: Int!
+}
+
+type NoDogError {
+    msg: String
+}
+
+union FindDogResult = Dog | NoDogError
 """
 
 val items = listOf(
@@ -314,6 +330,13 @@ class Query : GraphQLQueryResolver, ListListResolver<String>() {
     fun throwsIllegalArgumentException(): String {
         throw IllegalArgumentException("Expected")
     }
+
+    fun allDogs(): List<Dog> = listOf(LabradorRetriever("Hershey", "chocolate", 42, 3.14159f))
+
+    fun findSuitableDog(preferredColor: String, minimumFluffiness: Int): Any =
+        allDogs()
+            .firstOrNull { it.color == preferredColor && it.fluffiness >= minimumFluffiness }
+            ?: NoDogError("No $preferredColor-colored dog found that is sufficiently fluffy")
 }
 
 class UnusedRootResolver : GraphQLQueryResolver
@@ -410,6 +433,15 @@ class MockPart(private val name: String, private val content: String) : Part {
     override fun delete() = throw IllegalArgumentException("Not supported")
 }
 
+interface Dog {
+    val name: String
+    val color: String
+    val fluffiness: Int
+}
+interface Retriever : Dog { val speed: Float }
+class LabradorRetriever(override val name: String, override val color: String, override val fluffiness: Int, override val speed: Float) : Retriever
+class NoDogError(val msg: String)
+
 val customScalarId = GraphQLScalarType.newScalar()
     .name("ID")
     .description("Overrides built-in ID")
diff --git a/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt b/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt
index 6ca764c1..5d4dc51d 100644
--- a/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt
+++ b/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt
@@ -178,6 +178,22 @@ class EndToEndTest {
         assertNotNull(data["itemByUUID"])
     }
 
+    @Test
+    fun `generated schema should handle union types with deep hierarchy`() {
+        val data = assertNoGraphQlErrors(gql) {
+            """
+            {
+                findSuitableDog(preferredColor: "chocolate", minimumFluffiness: 31) {
+                    ... on Dog { name }
+                    ... on NoDogError { msg }
+                }
+            }
+            """
+        }
+
+        assertNotNull(data["findSuitableDog"])
+    }
+
     @Test
     fun `generated schema should handle non nullable scalar types`() {
         val fileParts = listOf(MockPart("test.doc", "Hello"), MockPart("test.doc", "World"))