Skip to content

Commit d56121f

Browse files
authored
Merge pull request #904 from graphql-java-kickstart/feature/304-sourceless-resolvers
Allow resolver methods that leave out the source object
2 parents 84dcb8e + e0dc4f4 commit d56121f

6 files changed

Lines changed: 689 additions & 34 deletions

File tree

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import graphql.kickstart.tools.resolver.FieldResolverScanner
44
import graphql.kickstart.tools.util.GraphQLRootResolver
55
import graphql.kickstart.tools.util.JavaType
66
import graphql.kickstart.tools.util.eraseUnboundedWildcards
7-
import graphql.kickstart.tools.util.unwrap
87
import org.apache.commons.lang3.reflect.TypeUtils
98
import java.lang.reflect.ParameterizedType
109

@@ -17,17 +16,13 @@ internal abstract class ResolverInfo {
1716
?: resolver.javaClass
1817
}
1918

20-
internal interface DataClassTypeResolverInfo {
21-
val dataClassType: Class<out Any>
22-
}
23-
2419
internal class NormalResolverInfo(
2520
val resolver: GraphQLResolver<*>,
2621
options: SchemaParserOptions
27-
) : DataClassTypeResolverInfo, ResolverInfo() {
22+
) : ResolverInfo() {
2823

2924
val resolverType = getRealResolverClass(resolver, options)
30-
override val dataClassType = findDataClass()
25+
val dataClassType = findDataClass()
3126

3227
private fun findDataClass(): Class<out Any> {
3328
val type = TypeUtils.getTypeArguments(resolverType, GraphQLResolver::class.java)[GraphQLResolver::class.java.typeParameters[0]]
@@ -63,9 +58,7 @@ internal class NormalResolverInfo(
6358
internal class MultiResolverInfo(
6459
val resolverInfoList: List<NormalResolverInfo>,
6560
private val dataClass: JavaType
66-
) : DataClassTypeResolverInfo, ResolverInfo() {
67-
68-
override val dataClassType = dataClass.unwrap()
61+
) : ResolverInfo() {
6962

7063
override fun getFieldSearches(): List<FieldResolverScanner.Search> {
7164
return resolverInfoList

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ internal class SchemaClassScanner(
548548

549549
fieldResolver.field.inputValueDefinitions.forEachIndexed { index, argument ->
550550
val parameter = fieldResolver.getMethodParameterType(index)?.let(fieldResolver.typeResolver::getRawClass) ?: return@forEachIndexed
551-
if (!isContextParameter(parameter, argument)) {
551+
if (!fieldResolverScanner.isContextParameter(parameter, argument)) {
552552
return@forEachIndexed
553553
}
554554

@@ -561,11 +561,6 @@ internal class SchemaClassScanner(
561561
}
562562
}
563563

564-
// the context class can also be an argument's type (e.g. a Map for an input object), so only spec scalars count for it
565-
private fun isContextParameter(parameter: Class<*>, argument: InputValueDefinition): Boolean =
566-
parameter in fieldResolverScanner.allowedTrailingParameterTypes &&
567-
(parameter != options.contextClass || argument.type.unwrap().name in ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS)
568-
569564
private fun handleFoundType(match: TypeClassMatcher.Match) = when (match) {
570565
is TypeClassMatcher.ScalarMatch -> handleFoundScalarType(match.type)
571566
is TypeClassMatcher.ValidMatch -> handleFoundType(match.type, match.javaType, match.reference)

‎src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt‎

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import graphql.kickstart.tools.RootResolverInfo
77
import graphql.kickstart.tools.SchemaParserOptions
88
import graphql.kickstart.tools.util.*
99
import graphql.language.FieldDefinition
10+
import graphql.language.InputValueDefinition
1011
import graphql.schema.DataFetchingEnvironment
12+
import graphql.schema.idl.ScalarInfo
1113
import kotlinx.coroutines.channels.ReceiveChannel
1214
import org.apache.commons.lang3.ClassUtils
1315
import org.apache.commons.lang3.reflect.FieldUtils
@@ -25,7 +27,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
2527

2628
private val log = LoggerFactory.getLogger(javaClass)
2729

28-
val allowedTrailingParameterTypes = listOfNotNull(DataFetchingEnvironment::class.java, GraphQLContext::class.java, options.contextClass)
30+
private val allowedTrailingParameterTypes = listOfNotNull(DataFetchingEnvironment::class.java, GraphQLContext::class.java, options.contextClass)
2931

3032
private val methodsByNameCache = mutableMapOf<Pair<Class<out Any>, Boolean>, Map<String, List<Method>>>()
3133

@@ -39,7 +41,10 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
3941
throw FieldResolverError("Found more than one matching resolver for field '$field': $found")
4042
}
4143

42-
return found.firstOrNull() ?: missingFieldResolver(field, searches, scanProperties)
44+
return found.firstOrNull()
45+
?: missingFieldResolver(field)
46+
?: findSourcelessFieldResolver(field, searches)
47+
?: throw FieldResolverError(getMissingFieldMessage(field, searches, scanProperties))
4348
}
4449

4550
private fun findFieldResolver(field: FieldDefinition, search: Search, scanProperties: Boolean): FieldResolver? {
@@ -71,7 +76,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
7176
}
7277
}
7378

74-
private fun missingFieldResolver(field: FieldDefinition, searches: List<Search>, scanProperties: Boolean): FieldResolver {
79+
private fun missingFieldResolver(field: FieldDefinition): FieldResolver? {
7580
@Suppress("DEPRECATION")
7681
return if (options.allowUnimplementedResolvers
7782
|| options.missingResolverDataFetcher != null
@@ -82,13 +87,44 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
8287

8388
MissingFieldResolver(field, options)
8489
} else {
85-
throw FieldResolverError(getMissingFieldMessage(field, searches, scanProperties))
90+
null
8691
}
8792
}
8893

94+
private fun findSourcelessFieldResolver(field: FieldDefinition, searches: List<Search>): FieldResolver? =
95+
searches.firstNotNullOfOrNull { search ->
96+
val sourceType = search.requiredFirstParameterType ?: return@firstNotNullOfOrNull null
97+
val sourceless = search.withoutSourceParameter()
98+
findMethodByName(field, sourceless) { isSourcelessMethod(it, field, sourceless, sourceType) }?.let { method ->
99+
MethodFieldResolver(field, sourceless, options, method.apply(trySetAccessible(field, search.type)), scalarNames)
100+
}
101+
}
102+
103+
private fun isSourcelessMethod(method: Method, field: FieldDefinition, search: Search, sourceType: Class<*>): Boolean {
104+
val arguments = field.inputValueDefinitions
105+
val parameterTypes = method.parameterTypes
106+
107+
return method.parameterCountWithoutContinuation() > arguments.size &&
108+
arguments.indices.none { isContextParameter(parameterTypes[it], arguments[it]) } &&
109+
(arguments.isEmpty() || !couldTakeSource(parameterTypes[0], sourceType)) &&
110+
verifyMethodArguments(method, arguments.size, search)
111+
}
112+
113+
private fun couldTakeSource(parameterType: Class<*>, sourceType: Class<*>) =
114+
sourceType.isAssignableFrom(parameterType) || parameterType.isAssignableFrom(sourceType)
115+
116+
// the context class can also be an argument's type (e.g. a Map for an input object), so only spec scalars count for it
117+
fun isContextParameter(parameter: Class<*>, argument: InputValueDefinition): Boolean =
118+
parameter in allowedTrailingParameterTypes &&
119+
(parameter != options.contextClass || argument.type.unwrap().name in ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS)
120+
89121
private fun findResolverMethod(field: FieldDefinition, search: Search): Method? {
122+
val argumentCount = field.inputValueDefinitions.size + search.firstArgumentIndex
123+
return findMethodByName(field, search) { verifyMethodArguments(it, argumentCount, search) }
124+
}
125+
126+
private inline fun findMethodByName(field: FieldDefinition, search: Search, matches: (Method) -> Boolean): Method? {
90127
val methodsByName = getMethodsByName(search)
91-
val argumentCount = field.inputValueDefinitions.size + if (search.requiredFirstParameterType != null) 1 else 0
92128
val name = field.name
93129
val capitalizedName = name.replaceFirstChar(Char::titlecase)
94130

@@ -105,7 +141,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
105141
"getField$capitalizedName",
106142
"get${name.snakeToCamelCase()}"
107143
).firstNotNullOfOrNull { methodName ->
108-
methodsByName[methodName]?.find { verifyMethodArguments(it, argumentCount, search) }
144+
methodsByName[methodName]?.find { matches(it) }
109145
}
110146
}
111147

@@ -197,6 +233,9 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
197233
signatures.addAll(getMissingMethodSignatures(field, search, isBoolean, scannedProperties))
198234
isSubscription = isSubscription || search.isSubscription
199235
}
236+
searches.filter { it.requiredFirstParameterType != null }.forEach { search ->
237+
signatures.addAll(getMissingMethodSignatures(field, search.withoutSourceParameter(), isBoolean, false, "<environment or context>"))
238+
}
200239

201240
val sourceName = field.sourceLocation?.sourceName ?: "<unknown>"
202241
val sourceLocation = field.sourceLocation?.let { "$sourceName:${it.line}" } ?: "<unknown>"
@@ -206,7 +245,13 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
206245
if (isSubscription) "\n\nNote that a Subscription data fetcher must return a Publisher of events" else ""
207246
}
208247

209-
private fun getMissingMethodSignatures(field: FieldDefinition, search: Search, isBoolean: Boolean, scannedProperties: Boolean): List<String> {
248+
private fun getMissingMethodSignatures(
249+
field: FieldDefinition,
250+
search: Search,
251+
isBoolean: Boolean,
252+
scannedProperties: Boolean,
253+
trailingParameter: String? = null
254+
): List<String> {
210255
val baseType = search.type.unwrap()
211256
val signatures = mutableListOf<String>()
212257
val args = mutableListOf<String>()
@@ -217,6 +262,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
217262
}
218263

219264
args.addAll(field.inputValueDefinitions.map { "~${it.name}" })
265+
trailingParameter?.let(args::add)
220266

221267
val argString = args.joinToString(sep)
222268

@@ -239,6 +285,10 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
239285
val requiredFirstParameterType: Class<*>? = null
240286
) {
241287
val isSubscription get() = resolverInfo is RootResolverInfo && resolverInfo.isSubscription
288+
289+
val firstArgumentIndex get() = if (requiredFirstParameterType != null) 1 else 0
290+
291+
fun withoutSourceParameter() = copy(requiredFirstParameterType = null)
242292
}
243293
}
244294

‎src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt‎

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ internal class MethodFieldResolver(
5252
val args = mutableListOf<ArgumentPlaceholder>()
5353
val mapper = objectMapper
5454

55-
// Add source argument if this is a resolver (but not a root resolver)
5655
if (this.search.requiredFirstParameterType != null) {
5756
val expectedType = this.search.requiredFirstParameterType
5857

@@ -103,7 +102,7 @@ internal class MethodFieldResolver(
103102
}
104103

105104
// Add DataFetchingEnvironment/Context arguments, stopping before the Continuation parameter of a suspend function
106-
val firstTrailingParameter = field.inputValueDefinitions.size + getIndexOffset()
105+
val firstTrailingParameter = field.inputValueDefinitions.size + search.firstArgumentIndex
107106
method.parameterTypes.slice(firstTrailingParameter until numberOfParameters).forEach { parameterType ->
108107
when (parameterType) {
109108
options.contextClass -> args.add { environment ->
@@ -188,15 +187,7 @@ internal class MethodFieldResolver(
188187
private fun getResolvedReturnType(): JavaType =
189188
typeResolver.resolve(if (search.isSubscription) returnType.asPublisher() else returnType)
190189

191-
private fun getIndexOffset(): Int {
192-
return if (resolverInfo is DataClassTypeResolverInfo && !method.declaringClass.isAssignableFrom(resolverInfo.dataClassType)) {
193-
1
194-
} else {
195-
0
196-
}
197-
}
198-
199-
private fun getJavaMethodParameterIndex(index: Int) = index + getIndexOffset()
190+
private fun getJavaMethodParameterIndex(index: Int) = index + search.firstArgumentIndex
200191

201192
internal fun getMethodParameterType(index: Int): JavaType? {
202193
val methodIndex = getJavaMethodParameterIndex(index)

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,27 @@ class MethodFieldResolverTest {
364364
fun thumbnail(image: Image, env: DataFetchingEnvironment?) = env.toString()
365365
}
366366

367+
@Test
368+
fun `should pass the source to a resolver method declared by the data class itself`() {
369+
val schema = SchemaParser.newParser()
370+
.schemaString("type Query { badge: Badge! } type Badge { size(scale: Int!): Int! }")
371+
.resolvers(BadgeQuery(), Badge("resolver"))
372+
.build()
373+
.makeExecutableSchema()
374+
375+
val data = assertNoGraphQlErrors(GraphQL.newGraphQL(schema).build()) { "{ badge { size(scale: 3) } }" }
376+
377+
assertEquals(data, mapOf("badge" to mapOf("size" to 15)))
378+
}
379+
380+
class BadgeQuery : GraphQLQueryResolver {
381+
fun badge() = Badge("badge")
382+
}
383+
384+
class Badge(val label: String) : GraphQLResolver<Badge> {
385+
fun size(badge: Badge, scale: Int) = badge.label.length * scale
386+
}
387+
367388
sealed class Maybe<out T> {
368389
data class Some<T>(val value: T) : Maybe<T>()
369390
object None : Maybe<Nothing>()

0 commit comments

Comments
 (0)