Skip to content

Commit 21f3796

Browse files
authored
Merge pull request #892 from graphql-java-kickstart/bugfix/parameterized-dictionary-type
Resolve union and interface values by the raw class of a parameterized type
2 parents c024bc3 + c903ff3 commit 21f3796

2 files changed

Lines changed: 92 additions & 4 deletions

File tree

‎src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,29 @@ package graphql.kickstart.tools
33
import graphql.TypeResolutionEnvironment
44
import graphql.kickstart.tools.util.BiMap
55
import graphql.kickstart.tools.util.JavaType
6+
import graphql.kickstart.tools.util.unwrap
67
import graphql.language.TypeDefinition
78
import graphql.schema.GraphQLInterfaceType
89
import graphql.schema.GraphQLObjectType
910
import graphql.schema.GraphQLUnionType
1011
import graphql.schema.TypeResolver
12+
import java.lang.reflect.ParameterizedType
1113

1214
/**
1315
* @author Andrew Potter
1416
*/
1517
internal abstract class DictionaryTypeResolver(
1618
private val dictionary: BiMap<JavaType, TypeDefinition<*>>
1719
) : TypeResolver {
20+
// Raw class of each parameterized entry, e.g. Box for Box<String>, unless several entries share it
21+
private val rawClassDictionary by lazy {
22+
dictionary.entries
23+
.filter { it.key is ParameterizedType }
24+
.groupBy({ it.key.unwrap() }, { it.value })
25+
.mapNotNull { (clazz, definitions) -> definitions.singleOrNull()?.let { clazz to it } }
26+
.toMap()
27+
}
28+
1829
private fun <T> getTypeDefinition(clazz: Class<T>): TypeDefinition<*>? {
1930
return dictionary[clazz]
2031
?: (if (clazz.superclass == null) null else getTypeDefinition(clazz.superclass))
@@ -24,7 +35,11 @@ internal abstract class DictionaryTypeResolver(
2435
override fun getType(env: TypeResolutionEnvironment): GraphQLObjectType? {
2536
val clazz = env.getObject<Any>()!!.javaClass
2637
val name = getTypeDefinition(clazz)?.name ?: clazz.simpleName
27-
return env.schema.getObjectType(name) ?: throw TypeResolverError(getError(name))
38+
return env.schema.getType(name) as? GraphQLObjectType
39+
?: rawClassDictionary[clazz]?.let { env.schema.getType(it.name) as? GraphQLObjectType }
40+
// throws graphql-java's AssertException when the name is a type other than an object type
41+
?: env.schema.getObjectType(name)
42+
?: throw TypeResolverError(getError(name))
2843
}
2944

3045
abstract fun getError(name: String): String

‎src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt‎

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package graphql.kickstart.tools
22

33
import graphql.GraphQL
4+
import org.junit.Assert.assertThrows
45
import org.junit.Test
56

67
class GenericObjectTypesTest {
@@ -12,8 +13,11 @@ class GenericObjectTypesTest {
1213
"""
1314
type Query {
1415
test: TestClass!
16+
anyMatch: Match!
1517
}
1618
19+
union Match = StringListEntityMatch | StringEntityMatch
20+
1721
type TestClass {
1822
match: StringListEntityMatch!
1923
single: StringEntityMatch!
@@ -32,7 +36,7 @@ class GenericObjectTypesTest {
3236
.resolvers(EntityMatchQueryResolver())
3337
.build()
3438
.makeExecutableSchema()
35-
.assertEveryFieldResolves()
39+
.assertEveryFieldResolves(except = setOf("Query.anyMatch"))
3640
val gql = GraphQL.newGraphQL(schema).build()
3741

3842
val data = assertNoGraphQlErrors(gql) {
@@ -50,6 +54,9 @@ class GenericObjectTypesTest {
5054
"match" to mapOf("text" to "x y", "value" to listOf("x", "y")),
5155
"single" to mapOf("text" to "z", "value" to "z")
5256
))
57+
58+
val error = assertThrows(TypeResolverError::class.java) { gql.execute("query { anyMatch { __typename } }") }
59+
assertEquals(error.message, "Expected object type with name 'EntityMatch' to exist for union 'Match', but it doesn't!")
5360
}
5461

5562
@Test
@@ -60,14 +67,22 @@ class GenericObjectTypesTest {
6067
type Query {
6168
characterHit: CharacterHit!
6269
characterHitPage: CharacterHitPage!
70+
lastPage: Result!
71+
topHit: Scored!
72+
}
73+
74+
union Result = CharacterHitPage | Human
75+
76+
interface Scored {
77+
score: Float!
6378
}
6479
6580
type CharacterHitPage {
6681
content: [CharacterHit!]!
6782
totalElements: Int!
6883
}
6984
70-
type CharacterHit {
85+
type CharacterHit implements Scored {
7186
score: Float!
7287
source: Character!
7388
}
@@ -110,6 +125,13 @@ class GenericObjectTypesTest {
110125
}
111126
}
112127
}
128+
lastPage {
129+
... on CharacterHitPage { totalElements }
130+
}
131+
topHit {
132+
score
133+
... on CharacterHit { source { name } }
134+
}
113135
}
114136
"""
115137
}
@@ -122,13 +144,56 @@ class GenericObjectTypesTest {
122144
mapOf("source" to mapOf("name" to "R2-D2", "primaryFunction" to "Astromech"))
123145
)
124146
))
147+
assertEquals(data["lastPage"], mapOf("totalElements" to 1))
148+
assertEquals(data["topHit"], mapOf("score" to 0.5, "source" to mapOf("name" to "Luke")))
149+
}
150+
151+
@Test
152+
fun `a raw class keeps its own type when a parameterized entry shares it`() {
153+
val schema = SchemaParser.newParser()
154+
.schemaString(
155+
"""
156+
type Query {
157+
single: StringEntityMatch!
158+
anyMatch: Match!
159+
}
160+
161+
union Match = StringEntityMatch | AnyEntityMatch
162+
163+
type StringEntityMatch {
164+
text: String!
165+
value: String
166+
}
167+
168+
type AnyEntityMatch {
169+
text: String!
170+
}
171+
""")
172+
.resolvers(RawEntityMatchQueryResolver())
173+
.dictionary("AnyEntityMatch", EntityMatch::class.java)
174+
.build()
175+
.makeExecutableSchema()
176+
.assertEveryFieldResolves()
177+
val gql = GraphQL.newGraphQL(schema).build()
178+
179+
val data = assertNoGraphQlErrors(gql) { "query { anyMatch { __typename } }" }
180+
181+
assertEquals(data["anyMatch"], mapOf("__typename" to "AnyEntityMatch"))
125182
}
126183

127184
class EntityMatchQueryResolver : GraphQLQueryResolver {
128185
fun test(): TestClass = TestClass(
129186
match = EntityMatch("x y", listOf("x", "y")),
130187
single = EntityMatch("z", "z")
131188
)
189+
190+
fun anyMatch(): Any = EntityMatch("z", "z")
191+
}
192+
193+
class RawEntityMatchQueryResolver : GraphQLQueryResolver {
194+
fun single(): EntityMatch<String> = EntityMatch("z", "z")
195+
196+
fun anyMatch(): Any = EntityMatch("z", "z")
132197
}
133198

134199
data class TestClass(val match: EntityMatch<List<String>>, val single: EntityMatch<String>)
@@ -141,11 +206,19 @@ class GenericObjectTypesTest {
141206
listOf(Hit(Human("Luke", "Tatooine"), 1.0), Hit(Droid("R2-D2", "Astromech"), 1.0)),
142207
2
143208
)
209+
210+
fun lastPage(): Any = Page(listOf(Hit(Droid("R2-D2", "Astromech"), 1.0)), 1)
211+
212+
fun topHit(): Scored = Hit(Human("Luke", "Tatooine"), 0.5)
144213
}
145214

146215
class Page<T>(val content: List<T>, val totalElements: Int)
147216

148-
class Hit<T>(val source: T, val score: Double)
217+
interface Scored {
218+
val score: Double
219+
}
220+
221+
class Hit<T>(val source: T, override val score: Double) : Scored
149222

150223
interface Character {
151224
val name: String

0 commit comments

Comments
 (0)