Skip to content

Commit d30b8eb

Browse files
authored
Merge pull request #835 from graphql-java-kickstart/bugfix/318
Fix suspend resolver context and Kotlin extension resolvers
2 parents e656974 + 0fdfd99 commit d30b8eb

5 files changed

Lines changed: 99 additions & 28 deletions

File tree

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

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import org.apache.commons.lang3.reflect.TypeUtils
1616
import org.reactivestreams.Publisher
1717
import org.slf4j.LoggerFactory
1818
import java.lang.reflect.*
19-
import kotlin.reflect.full.valueParameters
20-
import kotlin.reflect.jvm.javaType
19+
import kotlin.reflect.full.extensionReceiverParameter
2120
import kotlin.reflect.jvm.kotlinFunction
2221

2322
/**
@@ -167,31 +166,23 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
167166
it == search.requiredFirstParameterType || method.declaringClass.typeParameters.contains(it)
168167
} ?: false
169168
} else {
170-
true
169+
// an extension receiver can only take the source object
170+
!isExtensionFunction(method)
171171
}
172172

173-
val methodParameterCount = getMethodParameterCount(method)
174-
val methodLastParameter = getMethodLastParameter(method)
173+
val methodParameterCount = method.parameterCountWithoutContinuation()
174+
val methodLastParameter = method.parameterTypes.getOrNull(methodParameterCount - 1)
175175

176176
val correctParameterCount = methodParameterCount == requiredCount ||
177177
(methodParameterCount == (requiredCount + 1) && allowedLastArgumentTypes.contains(methodLastParameter))
178178
return correctParameterCount && appropriateFirstParameter
179179
}
180180

181-
private fun getMethodParameterCount(method: Method): Int {
181+
private fun isExtensionFunction(method: Method): Boolean {
182182
return try {
183-
method.kotlinFunction?.valueParameters?.size ?: method.parameterCount
183+
method.kotlinFunction?.extensionReceiverParameter != null
184184
} catch (e: InternalError) {
185-
method.parameterCount
186-
}
187-
}
188-
189-
private fun getMethodLastParameter(method: Method): Type? {
190-
return try {
191-
method.kotlinFunction?.valueParameters?.lastOrNull()?.type?.javaType
192-
?: method.parameterTypes.lastOrNull()
193-
} catch (e: InternalError) {
194-
method.parameterTypes.lastOrNull()
185+
false
195186
}
196187
}
197188

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

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import graphql.kickstart.tools.SchemaParserOptions.GenericWrapper
77
import graphql.kickstart.tools.util.JavaType
88
import graphql.kickstart.tools.util.coroutineScope
99
import graphql.kickstart.tools.util.futureValueType
10+
import graphql.kickstart.tools.util.isSuspendFunction
11+
import graphql.kickstart.tools.util.parameterCountWithoutContinuation
1012
import graphql.kickstart.tools.util.typeArgument
1113
import graphql.kickstart.tools.util.unwrap
1214
import graphql.language.*
@@ -26,7 +28,6 @@ import java.lang.reflect.Method
2628
import java.util.*
2729
import java.util.function.Supplier
2830
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
29-
import kotlin.reflect.full.valueParameters
3031
import kotlin.reflect.jvm.javaType
3132
import kotlin.reflect.jvm.kotlinFunction
3233

@@ -43,7 +44,7 @@ internal class MethodFieldResolver(
4344
private val log = LoggerFactory.getLogger(javaClass)
4445

4546
private val isSuspendFunction = method.isSuspendFunction()
46-
private val numberOfParameters = method.kotlinFunction?.valueParameters?.size ?: method.parameterCount
47+
private val numberOfParameters = method.parameterCountWithoutContinuation()
4748
private val hasAdditionalParameter = numberOfParameters == (field.inputValueDefinitions.size + getIndexOffset() + 1)
4849

4950
override fun createDataFetcher(): DataFetcher<*> {
@@ -102,7 +103,8 @@ internal class MethodFieldResolver(
102103

103104
// Add DataFetchingEnvironment/Context argument
104105
if (this.hasAdditionalParameter) {
105-
when (this.method.parameterTypes.last()) {
106+
// suspend functions have a trailing Continuation parameter
107+
when (this.method.parameterTypes[numberOfParameters - 1]) {
106108
null -> throw ResolverError("Expected at least one argument but got none, this is most likely a bug with graphql-java-tools")
107109
options.contextClass -> args.add { environment ->
108110
val context: Any? = environment.graphQlContext[options.contextClass]
@@ -282,14 +284,6 @@ private class CompareGenericWrappers {
282284
}
283285
}
284286

285-
private fun Method.isSuspendFunction(): Boolean {
286-
return try {
287-
this.kotlinFunction?.isSuspend == true
288-
} catch (e: InternalError) {
289-
false
290-
}
291-
}
292-
293287
private suspend inline fun invokeSuspend(target: Any, resolverMethod: Method, args: Array<Any?>): Any? {
294288
return suspendCoroutineUninterceptedOrReturn { continuation ->
295289
invoke(resolverMethod, target, args + continuation)

‎src/main/kotlin/graphql/kickstart/tools/util/Utils.kt‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import java.lang.reflect.ParameterizedType
1212
import java.lang.reflect.Proxy
1313
import java.util.concurrent.CompletionStage
1414
import java.util.concurrent.Future
15+
import kotlin.reflect.jvm.kotlinFunction
1516

1617
/**
1718
* @author Andrew Potter
@@ -59,6 +60,18 @@ internal val Class<*>.declaredNonProxyMethods: List<JavaMethod>
5960
}
6061
}
6162

63+
internal fun JavaMethod.isSuspendFunction(): Boolean {
64+
return try {
65+
this.kotlinFunction?.isSuspend == true
66+
} catch (e: InternalError) {
67+
false
68+
}
69+
}
70+
71+
// the trailing Continuation of a suspend function isn't a resolver argument
72+
internal fun JavaMethod.parameterCountWithoutContinuation(): Int =
73+
if (isSuspendFunction()) parameterCount - 1 else parameterCount
74+
6275
internal fun getDocumentation(node: AbstractNode<*>, options: SchemaParserOptions): String? =
6376
when {
6477
node is AbstractDescribedNode<*> && node.description != null -> node.description?.content

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import graphql.kickstart.tools.resolver.FieldResolverScanner
66
import graphql.kickstart.tools.resolver.MethodFieldResolver
77
import graphql.kickstart.tools.resolver.PropertyFieldResolver
88
import graphql.language.FieldDefinition
9+
import graphql.language.InputValueDefinition
910
import graphql.language.TypeName
1011
import graphql.relay.Connection
1112
import graphql.relay.DefaultConnection
1213
import graphql.relay.DefaultPageInfo
14+
import graphql.schema.DataFetchingEnvironment
1315
import kotlinx.coroutines.ExperimentalCoroutinesApi
1416
import org.junit.Test
1517
import java.util.*
@@ -98,6 +100,21 @@ class FieldResolverScannerTest {
98100
Locale.setDefault(default)
99101
}
100102

103+
@Test
104+
fun `scanner ignores extension functions on root resolvers`() {
105+
val resolver = RootResolverInfo(listOf(UserQuery(), ExtensionQuery()), options)
106+
val field = FieldDefinition.newFieldDefinition()
107+
.name("user")
108+
.type(TypeName("String"))
109+
.inputValueDefinition(InputValueDefinition("id", TypeName("ID")))
110+
.build()
111+
112+
val user = scanner.findFieldResolver(field, resolver)
113+
114+
assert(user is MethodFieldResolver)
115+
assertEquals((user as MethodFieldResolver).method.declaringClass, UserQuery::class.java)
116+
}
117+
101118
@Test
102119
fun `scanner finds field resolver methods in priority order`() {
103120
val resolverInfo = RootResolverInfo(listOf(PriorityQuery()), options)
@@ -123,6 +140,14 @@ class FieldResolverScannerTest {
123140
fun field1() {}
124141
}
125142

143+
class UserQuery : GraphQLQueryResolver {
144+
fun user(id: String): String = id
145+
}
146+
147+
class ExtensionQuery : GraphQLQueryResolver {
148+
fun DataFetchingEnvironment.user(): String = field.name
149+
}
150+
126151
class CamelCaseQuery1 : GraphQLQueryResolver {
127152
fun getHullType(): HullType = HullType()
128153
}

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,54 @@ class MethodFieldResolverDataFetcherTest {
240240
assertEquals(resolver.get(createEnvironment(DataClass(), context = context)), true)
241241
}
242242

243+
@Test
244+
fun `data fetcher passes context to suspend function if method has extra argument and context is specified`() {
245+
val context = GraphQLContext.newContext().build()
246+
val resolver = createFetcher("active", resolver = object : GraphQLResolver<DataClass> {
247+
suspend fun isActive(dataClass: DataClass, ctx: GraphQLContext): Boolean {
248+
return ctx == context
249+
}
250+
})
251+
252+
@Suppress("UNCHECKED_CAST")
253+
val future = resolver.get(createEnvironment(DataClass(), context = context)) as CompletableFuture<Boolean>
254+
assert(future.get())
255+
}
256+
257+
@Test
258+
fun `data fetcher passes custom context to suspend function if method has extra argument and custom context is specified`() {
259+
val customContext = ContextClass()
260+
val context = GraphQLContext.of(mapOf(ContextClass::class.java to customContext))
261+
val options = SchemaParserOptions.newOptions().contextClass(ContextClass::class).build()
262+
val resolver = createFetcher("active", options = options, resolver = object : GraphQLResolver<DataClass> {
263+
suspend fun isActive(dataClass: DataClass, ctx: ContextClass): Boolean {
264+
return ctx == customContext
265+
}
266+
})
267+
268+
@Suppress("UNCHECKED_CAST")
269+
val future = resolver.get(createEnvironment(DataClass(), context = context)) as CompletableFuture<Boolean>
270+
assert(future.get())
271+
}
272+
273+
@Test
274+
fun `data fetcher uses extension function on the data class`() {
275+
val resolver = createFetcher("name", object : GraphQLResolver<DataClass> {
276+
fun DataClass.name(): String = "extension $name"
277+
})
278+
279+
assertEquals(resolver.get(createEnvironment(DataClass())), "extension TestName")
280+
}
281+
282+
@Test
283+
fun `data fetcher passes environment to extension function if method has extra argument`() {
284+
val resolver = createFetcher("active", object : GraphQLResolver<DataClass> {
285+
fun DataClass.isActive(env: DataFetchingEnvironment): Boolean = env is DataFetchingEnvironment
286+
})
287+
288+
assertEquals(resolver.get(createEnvironment(DataClass())), true)
289+
}
290+
243291
@Test
244292
fun `data fetcher marshalls input object if required`() {
245293
val name = "correct name"

0 commit comments

Comments
 (0)