Skip to content

Commit 84dcb8e

Browse files
authored
Merge pull request #903 from graphql-java-kickstart/feature/254-nested-wrappers
Unwrap generic wrappers and futures nested in lists and other wrappers
2 parents a4736c9 + 37b0bd7 commit 84dcb8e

4 files changed

Lines changed: 622 additions & 62 deletions

File tree

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal class GenericTypeResolver(private val containingType: JavaType, private
2929
* - wildcards are replaced by their upper bound, e.g. `? extends Foo` becomes `Foo`
3030
* - primitives are replaced by their boxed class, e.g. `int` becomes `Integer`
3131
*/
32-
fun resolve(javaType: JavaType): JavaType {
32+
fun resolve(javaType: JavaType, onUnwrap: ((wrapped: ParameterizedType) -> Unit)? = null): JavaType {
3333
return when (val type = resolveTypeVariables(javaType)) {
3434
is ParameterizedType -> {
3535
val rawType = type.rawType
@@ -41,12 +41,17 @@ internal class GenericTypeResolver(private val containingType: JavaType, private
4141
throw IndexOutOfBoundsException("Generic type '${TypeUtils.toString(type)}' does not have a type argument at index ${wrapper.index}!")
4242
}
4343

44+
onUnwrap?.invoke(type)
4445
val unwrapsTo = wrapper.schemaWrapper.invoke(typeArguments[wrapper.index])
45-
resolve(unwrapsTo)
46+
resolve(unwrapsTo, onUnwrap)
4647
}
4748
is TypeVariable<*> -> error("Could not resolve type variable '${TypeUtils.toLongString(type)}' relative to ${TypeUtils.toString(containingType)}")
48-
is WildcardType -> type.upperBounds.firstOrNull()
49-
?: error("Unable to unwrap type, wildcard has no upper bound: $type")
49+
// Kotlin reflection gives a suspend function returning List<Foo> the type List<? extends Foo> when Foo is open.
50+
// A type variable bound stays as it is, since a method's own variable can't be resolved and a scalar doesn't need it
51+
is WildcardType -> when (val bound = type.upperBounds.firstOrNull() ?: error("Unable to unwrap type, wildcard has no upper bound: $type")) {
52+
is ParameterizedType -> resolve(bound, onUnwrap)
53+
else -> bound
54+
}
5055
is Class<*> -> if (type.isPrimitive) Primitives.wrap(type) else type
5156
else -> error("Unable to unwrap type: $type")
5257
}

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

Lines changed: 10 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package graphql.kickstart.tools.resolver
33
import com.fasterxml.jackson.databind.ObjectMapper
44
import graphql.GraphQLContext
55
import graphql.kickstart.tools.*
6-
import graphql.kickstart.tools.SchemaParserOptions.GenericWrapper
76
import graphql.kickstart.tools.util.JavaType
87
import graphql.kickstart.tools.util.coroutineScope
98
import graphql.kickstart.tools.util.futureValueType
@@ -25,7 +24,6 @@ import org.slf4j.LoggerFactory
2524
import java.lang.reflect.InvocationTargetException
2625
import java.lang.reflect.Method
2726
import java.util.*
28-
import java.util.concurrent.CompletionStage
2927
import java.util.concurrent.Future
3028
import java.util.function.Supplier
3129
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
@@ -133,13 +131,16 @@ internal class MethodFieldResolver(
133131
// Object (e.g. for a union) can't tell them apart, so then every future of a method whose return type is Future is checked
134132
val valueClass = (TypeUtils.getRawType(getResolvedReturnType(), null) ?: Any::class.java)
135133
.takeUnless { it == Any::class.java && TypeUtils.getRawType(returnType, null) == Future::class.java }
134+
val unwrapper = ReturnValueUnwrapper.create(
135+
method, returnType, valueClass, field.type, search.isSubscription, typeResolver, options.genericWrappers, scalarNames
136+
)
136137

137138
return if (numberOfParameters > 0 || isSuspendFunction) {
138139
// requires arguments and environment or is a suspend function
139-
MethodFieldResolverDataFetcher(createSourceResolver(), this.method, args, options, isSuspendFunction, valueClass)
140+
MethodFieldResolverDataFetcher(createSourceResolver(), this.method, args, options, isSuspendFunction, unwrapper)
140141
} else {
141142
// if there are no parameters an optimized version of the data fetcher can be used
142-
LightMethodFieldResolverDataFetcher(createSourceResolver(), this.method, options, valueClass)
143+
LightMethodFieldResolverDataFetcher(createSourceResolver(), this.method, unwrapper)
143144
}
144145
}
145146

@@ -217,7 +218,7 @@ internal class MethodFieldResolverDataFetcher(
217218
private val args: List<ArgumentPlaceholder>,
218219
private val options: SchemaParserOptions,
219220
private val isSuspendFunction: Boolean,
220-
private val valueClass: Class<*>?
221+
private val unwrapper: ReturnValueUnwrapper
221222
) : DataFetcher<Any> {
222223

223224
override fun get(environment: DataFetchingEnvironment): Any? {
@@ -229,12 +230,10 @@ internal class MethodFieldResolverDataFetcher(
229230
// which runs the block even if the context is already cancelled, hence ensureActive
230231
environment.coroutineScope().future(options.coroutineContextProvider.provide(), CoroutineStart.UNDISPATCHED) {
231232
ensureActive()
232-
invokeSuspend(source, method, args)?.transformWithGenericWrapper(options.genericWrappers) { environment }
233-
.requireAwaitable(valueClass, method, isSuspendFunction = true)
233+
invokeSuspend(source, method, args)?.let { unwrapper.unwrapSuspending(it) { environment } }
234234
}
235235
} else {
236-
invoke(method, source, args)?.transformWithGenericWrapper(options.genericWrappers) { environment }
237-
.requireAwaitable(valueClass, method, isSuspendFunction = false)
236+
invoke(method, source, args)?.let { unwrapper.unwrap(it) { environment } }
238237
}
239238
}
240239

@@ -253,15 +252,13 @@ internal class MethodFieldResolverDataFetcher(
253252
internal class LightMethodFieldResolverDataFetcher(
254253
private val sourceResolver: SourceResolver,
255254
private val method: Method,
256-
private val options: SchemaParserOptions,
257-
private val valueClass: Class<*>?
255+
private val unwrapper: ReturnValueUnwrapper
258256
) : LightDataFetcher<Any?> {
259257

260258
override fun get(fieldDefinition: GraphQLFieldDefinition?, sourceObject: Any?, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? {
261259
val source = sourceResolver.resolve(null, sourceObject)
262260

263-
return invoke(method, source, emptyArray())?.transformWithGenericWrapper(options.genericWrappers, environmentSupplier)
264-
.requireAwaitable(valueClass, method, isSuspendFunction = false)
261+
return invoke(method, source, emptyArray())?.let { unwrapper.unwrap(it, environmentSupplier) }
265262
}
266263

267264
override fun get(environment: DataFetchingEnvironment): Any? {
@@ -277,51 +274,6 @@ internal class LightMethodFieldResolverDataFetcher(
277274
}
278275
}
279276

280-
private fun Any.transformWithGenericWrapper(
281-
genericWrappers: List<GenericWrapper>,
282-
environmentSupplier: Supplier<DataFetchingEnvironment>
283-
): Any? {
284-
val genericWrapper = genericWrappers
285-
.asSequence()
286-
.filter { it.type.isInstance(this) }
287-
.sortedWith(CompareGenericWrappers)
288-
.firstOrNull()
289-
?: return this
290-
291-
// returned as is, even if null, so a transformer can map a value to null
292-
return genericWrapper.transformer(this, environmentSupplier.get())
293-
}
294-
295-
// graphql-java only waits on a CompletionStage returned by a data fetcher, so any other future, or a future
296-
// that a suspend function completes with, would reach it as the field value
297-
private fun Any?.requireAwaitable(valueClass: Class<*>?, method: Method, isSuspendFunction: Boolean): Any? {
298-
if (this !is Future<*> || valueClass?.isInstance(this) == true) {
299-
return this
300-
}
301-
302-
val resolverName = "${method.declaringClass.name}.${method.name}"
303-
if (isSuspendFunction) {
304-
throw ResolverError("Suspend function '$resolverName' resolved to a ${javaClass.name}, which graphql-java can't wait on. Return its value instead.")
305-
}
306-
if (this !is CompletionStage<*>) {
307-
throw ResolverError(
308-
"Resolver method '$resolverName' resolved to a ${javaClass.name}, which graphql-java can't wait on. " +
309-
"Return a CompletionStage, such as a CompletableFuture, from the method or its generic wrapper transformer instead."
310-
)
311-
}
312-
313-
return this
314-
}
315-
316-
private class CompareGenericWrappers {
317-
companion object : Comparator<GenericWrapper> {
318-
override fun compare(w1: GenericWrapper, w2: GenericWrapper): Int = when {
319-
w1.type.isAssignableFrom(w2.type) -> 1
320-
else -> -1
321-
}
322-
}
323-
}
324-
325277
private suspend inline fun invokeSuspend(target: Any, resolverMethod: Method, args: Array<Any?>): Any? {
326278
return suspendCoroutineUninterceptedOrReturn { continuation ->
327279
invoke(resolverMethod, target, args + continuation)
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package graphql.kickstart.tools.resolver
2+
3+
import graphql.execution.DataFetcherResult
4+
import graphql.kickstart.tools.GenericTypeResolver
5+
import graphql.kickstart.tools.ResolverError
6+
import graphql.kickstart.tools.SchemaParserOptions.GenericWrapper
7+
import graphql.kickstart.tools.util.GraphQLLangType
8+
import graphql.kickstart.tools.util.JavaType
9+
import graphql.kickstart.tools.util.isSubtypeOf
10+
import graphql.kickstart.tools.util.unwrap
11+
import graphql.language.ListType
12+
import graphql.language.NonNullType
13+
import graphql.schema.DataFetchingEnvironment
14+
import graphql.schema.idl.ScalarInfo
15+
import kotlinx.coroutines.future.asDeferred
16+
import java.lang.reflect.Method
17+
import java.lang.reflect.ParameterizedType
18+
import java.lang.reflect.WildcardType
19+
import java.util.*
20+
import java.util.concurrent.CompletableFuture
21+
import java.util.concurrent.CompletionStage
22+
import java.util.concurrent.Future
23+
import java.util.function.Supplier
24+
25+
internal class ReturnValueUnwrapper private constructor(
26+
private val genericWrappers: List<GenericWrapper>,
27+
private val method: Method,
28+
private val valueClass: Class<*>?,
29+
private val nested: Step?
30+
) {
31+
32+
fun unwrap(value: Any, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? =
33+
unwrap(value, environmentSupplier, isSuspendFunction = false)
34+
35+
suspend fun unwrapSuspending(value: Any, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? {
36+
val unwrapped = unwrap(value, environmentSupplier, isSuspendFunction = true)
37+
// the coroutine's future would complete with the future made below the outermost value, which graphql-java
38+
// doesn't wait on. asDeferred, so cancelling the coroutine doesn't cancel a future the resolver returned
39+
return if (nested != null && unwrapped is CompletionStage<*>) unwrapped.asDeferred().await() else unwrapped
40+
}
41+
42+
private fun unwrap(value: Any, environmentSupplier: Supplier<DataFetchingEnvironment>, isSuspendFunction: Boolean): Any? =
43+
value.transformWithGenericWrapper(genericWrappers, environmentSupplier)
44+
.requireAwaitable(valueClass, method, isSuspendFunction)
45+
.then(nested, environmentSupplier)
46+
47+
companion object {
48+
fun create(
49+
method: Method,
50+
returnType: JavaType,
51+
valueClass: Class<*>?,
52+
fieldType: GraphQLLangType,
53+
isSubscription: Boolean,
54+
typeResolver: GenericTypeResolver,
55+
genericWrappers: List<GenericWrapper>,
56+
scalarNames: Set<String>
57+
): ReturnValueUnwrapper {
58+
val nested = when {
59+
// graphql-java completes a subscription's events one at a time
60+
isSubscription -> null
61+
!hasGenericWrapperBelow(returnType, genericWrappers) -> null
62+
fieldType.unwrap().name.let { it in scalarNames && it !in ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS } -> null
63+
else -> nestedSteps(fieldType, returnType, typeResolver, genericWrappers)
64+
}
65+
return ReturnValueUnwrapper(genericWrappers, method, valueClass, nested)
66+
}
67+
}
68+
}
69+
70+
private sealed interface Step {
71+
fun unwrap(value: Any?, environmentSupplier: Supplier<DataFetchingEnvironment>): Any?
72+
}
73+
74+
private class Peel(private val genericWrappers: List<GenericWrapper>, private val next: Step?) : Step {
75+
override fun unwrap(value: Any?, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? =
76+
value?.transformWithGenericWrapper(genericWrappers, environmentSupplier).then(next, environmentSupplier)
77+
}
78+
79+
private class EachElement(private val element: Step) : Step {
80+
override fun unwrap(value: Any?, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? =
81+
if (value is Iterable<*>) value.map { element.unwrap(it, environmentSupplier) }.awaitAll() else value
82+
}
83+
84+
private class DataOf(private val data: Step) : Step {
85+
@Suppress("UNCHECKED_CAST")
86+
override fun unwrap(value: Any?, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? =
87+
if (value is DataFetcherResult<*>) {
88+
data.unwrap(value.data, environmentSupplier).then { unwrapped -> (value as DataFetcherResult<Any?>).map { unwrapped } }
89+
} else {
90+
value
91+
}
92+
}
93+
94+
private class ValueOf(private val present: Step) : Step {
95+
override fun unwrap(value: Any?, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? =
96+
if (value is Optional<*>) present.unwrap(value.orElse(null), environmentSupplier) else value
97+
}
98+
99+
private sealed interface Unwrapped {
100+
class Wrapper(rawType: Class<*>) : Unwrapped {
101+
val isFuture = Future::class.java.isAssignableFrom(rawType) || CompletionStage::class.java.isAssignableFrom(rawType)
102+
}
103+
104+
object Data : Unwrapped
105+
object Present : Unwrapped
106+
object Elements : Unwrapped
107+
}
108+
109+
private fun nestedSteps(fieldType: GraphQLLangType, returnType: JavaType, typeResolver: GenericTypeResolver, genericWrappers: List<GenericWrapper>): Step? {
110+
val unwrapped = mutableListOf<Unwrapped>()
111+
val onUnwrap = { wrapped: ParameterizedType -> unwrapped += Unwrapped.Wrapper(wrapped.rawType as Class<*>) }
112+
113+
fun addLevel(graphQLType: GraphQLLangType, javaType: JavaType) {
114+
var type = typeResolver.resolve(javaType, onUnwrap)
115+
if (type is ParameterizedType && type.isSubtypeOf(DataFetcherResult::class.java)) {
116+
unwrapped += Unwrapped.Data
117+
type = typeResolver.resolve(type.actualTypeArguments.first(), onUnwrap)
118+
}
119+
if (type is ParameterizedType && type.isSubtypeOf(Optional::class.java)) {
120+
unwrapped += Unwrapped.Present
121+
type = typeResolver.resolve(type.actualTypeArguments.first(), onUnwrap)
122+
}
123+
when (graphQLType) {
124+
is NonNullType -> addLevel(graphQLType.type, type)
125+
is ListType -> if (type is ParameterizedType && type.isSubtypeOf(Iterable::class.java)) {
126+
unwrapped += Unwrapped.Elements
127+
addLevel(graphQLType.type, type.actualTypeArguments.first())
128+
}
129+
else -> Unit
130+
}
131+
}
132+
133+
fun isFutureReturnedByWrapperBefore(i: Int) =
134+
(unwrapped[i] as? Unwrapped.Wrapper)?.isFuture == true && (unwrapped.getOrNull(i - 1) as? Unwrapped.Wrapper)?.isFuture == false
135+
136+
addLevel(fieldType, typeResolver.resolve(returnType, onUnwrap))
137+
val awaited = unwrapped.filterIndexed { i, _ -> !isFutureReturnedByWrapperBefore(i) }
138+
val belowOutermost = if (awaited.firstOrNull() is Unwrapped.Wrapper) awaited.drop(1) else awaited
139+
return belowOutermost.foldRight(null as Step?) { it, next ->
140+
when (it) {
141+
is Unwrapped.Wrapper -> Peel(genericWrappers, next)
142+
Unwrapped.Data -> next?.let(::DataOf)
143+
Unwrapped.Present -> next?.let(::ValueOf)
144+
Unwrapped.Elements -> next?.let(::EachElement)
145+
}
146+
}
147+
}
148+
149+
private fun hasGenericWrapperBelow(type: JavaType, genericWrappers: List<GenericWrapper>): Boolean =
150+
type is ParameterizedType && type.actualTypeArguments.any { it.mentionsGenericWrapper(genericWrappers) }
151+
152+
private fun JavaType.mentionsGenericWrapper(genericWrappers: List<GenericWrapper>): Boolean = when (this) {
153+
is ParameterizedType -> genericWrappers.any { it.type == rawType } || actualTypeArguments.any { it.mentionsGenericWrapper(genericWrappers) }
154+
is WildcardType -> upperBounds.any { it.mentionsGenericWrapper(genericWrappers) }
155+
else -> false
156+
}
157+
158+
private fun Any?.then(next: Step?, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? =
159+
if (next == null) this else then { next.unwrap(it, environmentSupplier) }
160+
161+
private inline fun Any?.then(crossinline next: (Any?) -> Any?): Any? =
162+
if (this is CompletionStage<*>) thenCompose { next(it).asCompletionStage() } else next(this)
163+
164+
private fun List<Any?>.awaitAll(): Any {
165+
if (none { it is CompletionStage<*> }) {
166+
return this
167+
}
168+
val futures = map { it.asCompletionStage().toCompletableFuture() }
169+
return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { futures.map { it.join() } }
170+
}
171+
172+
@Suppress("UNCHECKED_CAST")
173+
private fun Any?.asCompletionStage(): CompletionStage<Any?> =
174+
this as? CompletionStage<Any?> ?: CompletableFuture.completedFuture(this)
175+
176+
private fun Any.transformWithGenericWrapper(
177+
genericWrappers: List<GenericWrapper>,
178+
environmentSupplier: Supplier<DataFetchingEnvironment>
179+
): Any? {
180+
// the JIT doesn't optimize away the sequence below on this call path, so values that aren't wrappers, which is
181+
// most of them, return before it's allocated
182+
if (genericWrappers.none { it.type.isInstance(this) }) {
183+
return this
184+
}
185+
val genericWrapper = genericWrappers
186+
.asSequence()
187+
.filter { it.type.isInstance(this) }
188+
.sortedWith(CompareGenericWrappers)
189+
.firstOrNull()
190+
?: return this
191+
192+
// returned as is, even if null, so a transformer can map a value to null
193+
return genericWrapper.transformer(this, environmentSupplier.get())
194+
}
195+
196+
// graphql-java only waits on a CompletionStage returned by a data fetcher, so any other future, or a future
197+
// that a suspend function completes with, would reach it as the field value
198+
private fun Any?.requireAwaitable(valueClass: Class<*>?, method: Method, isSuspendFunction: Boolean): Any? {
199+
if (this !is Future<*> || valueClass?.isInstance(this) == true) {
200+
return this
201+
}
202+
203+
val resolverName = "${method.declaringClass.name}.${method.name}"
204+
if (isSuspendFunction) {
205+
throw ResolverError("Suspend function '$resolverName' resolved to a ${javaClass.name}, which graphql-java can't wait on. Return its value instead.")
206+
}
207+
if (this !is CompletionStage<*>) {
208+
throw ResolverError(
209+
"Resolver method '$resolverName' resolved to a ${javaClass.name}, which graphql-java can't wait on. " +
210+
"Return a CompletionStage, such as a CompletableFuture, from the method or its generic wrapper transformer instead."
211+
)
212+
}
213+
214+
return this
215+
}
216+
217+
private class CompareGenericWrappers {
218+
companion object : Comparator<GenericWrapper> {
219+
override fun compare(w1: GenericWrapper, w2: GenericWrapper): Int = when {
220+
w1.type.isAssignableFrom(w2.type) -> 1
221+
else -> -1
222+
}
223+
}
224+
}

0 commit comments

Comments
 (0)