Skip to content

Commit 06e28fc

Browse files
authored
Merge branch 'master' into graphql-java-26
2 parents 377cc2c + d46d10c commit 06e28fc

3 files changed

Lines changed: 185 additions & 33 deletions

File tree

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

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,26 +48,19 @@ internal class NormalResolverInfo(
4848
}
4949
}
5050

51-
internal class MultiResolverInfo(val resolverInfoList: List<NormalResolverInfo>) : DataClassTypeResolverInfo, ResolverInfo() {
52-
override val dataClassType = findDataClass()
53-
54-
/**
55-
* Checks if all `ResolverInfo` instances are related to the same data type
56-
*/
57-
private fun findDataClass(): Class<out Any> {
58-
val dataClass = resolverInfoList.asSequence().map { it.dataClassType }.distinct().singleOrNull()
59-
60-
if (dataClass == null) {
61-
throw ResolverError("Resolvers may not use the same type.")
62-
} else {
63-
return dataClass
64-
}
65-
}
51+
/**
52+
* Combines all resolvers that apply to a data class, i.e. resolvers for the data class itself and for any of its supertypes.
53+
* Resolvers are searched in the given order, followed by the data class itself.
54+
*/
55+
internal class MultiResolverInfo(
56+
val resolverInfoList: List<NormalResolverInfo>,
57+
override val dataClassType: Class<out Any>
58+
) : DataClassTypeResolverInfo, ResolverInfo() {
6659

6760
override fun getFieldSearches(): List<FieldResolverScanner.Search> {
6861
return resolverInfoList
6962
.asSequence()
70-
.map { FieldResolverScanner.Search(it.resolverType, this, it.resolver, dataClassType) }
63+
.map { FieldResolverScanner.Search(it.resolverType, this, it.resolver, it.dataClassType) }
7164
.plus(FieldResolverScanner.Search(dataClassType, this, null))
7265
.toList()
7366
}

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

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import graphql.kickstart.tools.util.*
66
import graphql.language.*
77
import graphql.schema.GraphQLScalarType
88
import graphql.schema.idl.ScalarInfo
9+
import org.apache.commons.lang3.ClassUtils
910
import org.slf4j.LoggerFactory
1011
import java.lang.reflect.Method
1112

@@ -28,7 +29,6 @@ internal class SchemaClassScanner(
2829
private val subscriptionResolvers = resolvers.filterIsInstance<GraphQLSubscriptionResolver>()
2930

3031
private val resolverInfos = resolvers.asSequence().minus(queryResolvers).minus(mutationResolvers).minus(subscriptionResolvers).map { NormalResolverInfo(it, options) }.toList()
31-
private val resolverInfosByDataClass = this.resolverInfos.associateBy { it.dataClassType }
3232

3333
private val initialDictionary = initialDictionary.mapValues { InitialDictionaryEntry(it.value) }
3434
private val extensionDefinitions = allDefinitions.filterIsInstance<ObjectTypeExtensionDefinition>()
@@ -281,34 +281,47 @@ internal class SchemaClassScanner(
281281
}
282282
}
283283

284+
/**
285+
* Scan a new object for types that haven't been mapped yet.
286+
*/
287+
private fun scanQueueItemForPotentialMatches(item: QueueItem) {
288+
val resolverInfo = if (item.clazz == Object::class.java) {
289+
getResolverInfoFromTypeDictionary(item.type.name)
290+
?: throw SchemaClassScannerError("The GraphQL schema type '${item.type.name}' maps to a field of type java.lang.Object however there is no matching entry for this type in the type dictionary. You may need to add this type to the dictionary before building the schema.")
291+
} else {
292+
getResolverInfoFromDataClass(item.clazz)
293+
}
294+
295+
scanResolverInfoForPotentialMatches(item.type, resolverInfo)
296+
}
297+
284298
private fun getResolverInfoFromTypeDictionary(typeName: String): ResolverInfo? {
285299
val dictionaryType = initialDictionary[typeName]?.get()
286300
return if (dictionaryType != null) {
287-
resolverInfosByDataClass[dictionaryType] ?: DataClassResolverInfo(dictionaryType)
301+
getResolverInfoFromDataClass(dictionaryType)
288302
} else {
289303
null
290304
}
291305
}
292306

293307
/**
294-
* Scan a new object for types that haven't been mapped yet.
308+
* Find all resolvers for the data class or any of its supertypes, most specific first.
295309
*/
296-
private fun scanQueueItemForPotentialMatches(item: QueueItem) {
297-
val resolverInfoList = this.resolverInfos.filter { it.dataClassType == item.clazz }
298-
val resolverInfo: ResolverInfo = (if (resolverInfoList.size > 1) {
299-
MultiResolverInfo(resolverInfoList)
300-
} else {
301-
if (item.clazz == Object::class.java) {
302-
getResolverInfoFromTypeDictionary(item.type.name)
303-
} else {
304-
resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz)
305-
}
306-
})
307-
?: throw SchemaClassScannerError("The GraphQL schema type '${item.type.name}' maps to a field of type java.lang.Object however there is no matching entry for this type in the type dictionary. You may need to add this type to the dictionary before building the schema.")
308-
309-
scanResolverInfoForPotentialMatches(item.type, resolverInfo)
310+
private fun getResolverInfoFromDataClass(dataClass: JavaType): ResolverInfo {
311+
val resolverInfoList = resolverInfos
312+
.filter { it.dataClassType == dataClass || isResolverForSupertype(it, dataClass) }
313+
.sortedByDescending { ClassUtils.getAllSuperclasses(it.dataClassType).size + ClassUtils.getAllInterfaces(it.dataClassType).size }
314+
315+
return when {
316+
resolverInfoList.isEmpty() -> DataClassResolverInfo(dataClass)
317+
resolverInfoList.size == 1 && resolverInfoList.single().dataClassType == dataClass -> resolverInfoList.single()
318+
else -> MultiResolverInfo(resolverInfoList, dataClass.unwrap())
319+
}
310320
}
311321

322+
private fun isResolverForSupertype(resolverInfo: NormalResolverInfo, dataClass: JavaType) =
323+
dataClass is Class<*> && resolverInfo.dataClassType != Object::class.java && resolverInfo.dataClassType.isAssignableFrom(dataClass)
324+
312325
private fun scanResolverInfoForPotentialMatches(type: ObjectTypeDefinition, resolverInfo: ResolverInfo) {
313326
type.getExtendedFieldDefinitions(extensionDefinitions).forEach { field ->
314327
val fieldResolver = fieldResolverScanner.findFieldResolver(field, resolverInfo)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package graphql.kickstart.tools
2+
3+
import graphql.GraphQL
4+
import graphql.execution.AsyncExecutionStrategy
5+
import graphql.schema.GraphQLSchema
6+
import org.junit.Test
7+
8+
class SupertypeResolverTest {
9+
10+
private val schema: GraphQLSchema = SchemaParser.newParser()
11+
.schemaString(
12+
"""
13+
type Query {
14+
thing: Thing!
15+
otherThing: OtherThing!
16+
labelledThing: LabelledThing!
17+
}
18+
19+
type Thing {
20+
name: String!
21+
active: Boolean!
22+
label: String!
23+
description: String!
24+
}
25+
26+
type OtherThing {
27+
name: String!
28+
active: Boolean!
29+
label: String!
30+
description: String!
31+
}
32+
33+
type LabelledThing {
34+
label: String!
35+
}
36+
""")
37+
.resolvers(QueryResolver(), BaseResolver(), NamedResolver(), OtherThingResolver())
38+
.build()
39+
.makeExecutableSchema()
40+
private val gql: GraphQL = GraphQL.newGraphQL(schema)
41+
.queryExecutionStrategy(AsyncExecutionStrategy())
42+
.build()
43+
44+
@Test
45+
fun `resolvers for a superclass or interface should resolve fields of subclasses`() {
46+
val data = assertNoGraphQlErrors(gql) {
47+
"""
48+
query {
49+
thing {
50+
name
51+
active
52+
label
53+
description
54+
}
55+
}
56+
"""
57+
}
58+
59+
assertEquals(data["thing"], mapOf(
60+
"name" to "thing",
61+
"active" to true,
62+
"label" to "base label",
63+
"description" to "named description"
64+
))
65+
}
66+
67+
@Test
68+
fun `more specific resolvers should take precedence over supertype resolvers`() {
69+
val data = assertNoGraphQlErrors(gql) {
70+
"""
71+
query {
72+
otherThing {
73+
name
74+
active
75+
label
76+
description
77+
}
78+
}
79+
"""
80+
}
81+
82+
assertEquals(data["otherThing"], mapOf(
83+
"name" to "other thing",
84+
"active" to false,
85+
"label" to "other label",
86+
"description" to "named description"
87+
))
88+
}
89+
90+
@Test
91+
fun `supertype resolvers should take precedence over getters on the data class`() {
92+
val data = assertNoGraphQlErrors(gql) {
93+
"""
94+
query {
95+
labelledThing {
96+
label
97+
}
98+
}
99+
"""
100+
}
101+
102+
assertEquals(data["labelledThing"], mapOf(
103+
"label" to "base label"
104+
))
105+
}
106+
107+
class QueryResolver : GraphQLQueryResolver {
108+
fun thing(): Thing = Thing()
109+
fun otherThing(): OtherThing = OtherThing()
110+
fun labelledThing(): LabelledThing = LabelledThing()
111+
}
112+
113+
interface Named {
114+
val name: String
115+
}
116+
117+
abstract class Base : Named
118+
119+
class Thing : Base() {
120+
override val name = "thing"
121+
}
122+
123+
class OtherThing : Base() {
124+
override val name = "other thing"
125+
}
126+
127+
class LabelledThing : Base() {
128+
override val name = "labelled thing"
129+
val label = "getter label"
130+
}
131+
132+
class BaseResolver : GraphQLResolver<Base> {
133+
fun isActive(base: Base): Boolean = true
134+
fun label(base: Base): String = "base label"
135+
}
136+
137+
class NamedResolver : GraphQLResolver<Named> {
138+
fun label(named: Named): String = "named label"
139+
fun description(named: Named): String = "named description"
140+
}
141+
142+
class OtherThingResolver : GraphQLResolver<OtherThing> {
143+
fun isActive(otherThing: OtherThing): Boolean = false
144+
fun label(otherThing: OtherThing): String = "other label"
145+
}
146+
}

0 commit comments

Comments
 (0)