Skip to content

Commit cd3233c

Browse files
authored
Merge pull request #941 from graphql-java-kickstart/bugfix/938-future-returning-wrapper
Wait on a future inside a generic wrapper whose transformer returns a future
2 parents 1b314a3 + d54d052 commit cd3233c

3 files changed

Lines changed: 75 additions & 17 deletions

File tree

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ data class SchemaParserOptions internal constructor(
231231
constructor(type: Class<*>, index: Int) : this(type, index, { x, _ -> x })
232232
constructor(type: KClass<*>, index: Int) : this(type.java, index, { x, _ -> x })
233233

234+
internal val transformerReturnsCompletionStage
235+
get() = transformer is CompletionStageTransformer<*>
236+
234237
companion object {
235238
@Suppress("UNCHECKED_CAST")
236239
@JvmStatic
@@ -289,6 +292,34 @@ data class SchemaParserOptions internal constructor(
289292
): GenericWrapper where T : Any {
290293
return listCollectionWithTransformer(type.java, index, transformer)
291294
}
295+
296+
/**
297+
* A wrapper whose [transformer] returns a future of its content, such as Reactor's `Mono::toFuture`. Unlike
298+
* [withTransformer], a future declared as the content, as in `Mono<CompletableFuture<Item>>`, is then waited on too.
299+
*/
300+
@JvmStatic
301+
fun <T> withCompletionStageTransformer(
302+
type: Class<T>,
303+
index: Int,
304+
transformer: (T) -> CompletionStage<*>?
305+
): GenericWrapper where T : Any {
306+
return GenericWrapper(type, index, CompletionStageTransformer(type, transformer))
307+
}
308+
309+
fun <T> withCompletionStageTransformer(
310+
type: KClass<T>,
311+
index: Int,
312+
transformer: (T) -> CompletionStage<*>?
313+
): GenericWrapper where T : Any {
314+
return withCompletionStageTransformer(type.java, index, transformer)
315+
}
292316
}
293317
}
294318
}
319+
320+
private class CompletionStageTransformer<T : Any>(
321+
private val type: Class<T>,
322+
private val transformer: (T) -> CompletionStage<*>?
323+
) : (Any, DataFetchingEnvironment) -> Any? {
324+
override fun invoke(value: Any, environment: DataFetchingEnvironment): Any? = transformer(type.cast(value))
325+
}

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

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,14 @@ internal class ReturnValueUnwrapper private constructor(
2323
) {
2424

2525
fun unwrap(value: Any, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? {
26-
val unwrapped = awaitable.requireOutermost(value.transformWithGenericWrapper(genericWrappers, environmentSupplier))
27-
return if (isOutermostFuture) unwrapped.then(nested, environmentSupplier) else unwrapped.thenUnlessNextAwaits(nested, environmentSupplier)
26+
val wrapper = genericWrappers.wrapperOf(value)
27+
val transformed = if (wrapper == null) value else wrapper.transformer(value, environmentSupplier.get())
28+
val unwrapped = awaitable.requireOutermost(transformed)
29+
return if (isOutermostFuture || wrapper?.transformerReturnsCompletionStage == true) {
30+
unwrapped.then(nested, environmentSupplier)
31+
} else {
32+
unwrapped.thenUnlessNextAwaits(nested, environmentSupplier)
33+
}
2834
}
2935

3036
suspend fun unwrapSuspending(value: Any, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? {
@@ -69,7 +75,11 @@ private class Await(private val wrapper: GenericWrapper, private val awaitable:
6975
private class Peel(private val wrapper: GenericWrapper, private val awaitable: Awaitable, private val next: Step?) : Step {
7076
override fun unwrap(value: Any?, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? {
7177
val transformed = value?.let { wrapper.transformer(it, environmentSupplier.get()) }
72-
return if (next is Await) next.unwrap(transformed, environmentSupplier) else awaitable.requireNested(transformed).then(next, environmentSupplier)
78+
return if (next is Await && !wrapper.transformerReturnsCompletionStage) {
79+
next.unwrap(transformed, environmentSupplier)
80+
} else {
81+
awaitable.requireNested(transformed).then(next, environmentSupplier)
82+
}
7383
}
7484
}
7585

@@ -133,24 +143,16 @@ private fun List<Any?>.awaitAll(): Any {
133143
private fun Any?.asCompletionStage(): CompletionStage<Any?> =
134144
this as? CompletionStage<Any?> ?: CompletableFuture.completedFuture(this)
135145

136-
private fun Any.transformWithGenericWrapper(
137-
genericWrappers: List<GenericWrapper>,
138-
environmentSupplier: Supplier<DataFetchingEnvironment>
139-
): Any? {
146+
private fun List<GenericWrapper>.wrapperOf(value: Any): GenericWrapper? {
140147
// the JIT doesn't optimize away the sequence below on this call path, so values that aren't wrappers, which is
141148
// most of them, return before it's allocated
142-
if (genericWrappers.none { it.type.isInstance(this) }) {
143-
return this
149+
if (none { it.type.isInstance(value) }) {
150+
return null
144151
}
145-
val genericWrapper = genericWrappers
146-
.asSequence()
147-
.filter { it.type.isInstance(this) }
152+
return asSequence()
153+
.filter { it.type.isInstance(value) }
148154
.sortedWith(CompareGenericWrappers)
149155
.firstOrNull()
150-
?: return this
151-
152-
// returned as is, even if null, so a transformer can map a value to null
153-
return genericWrapper.transformer(this, environmentSupplier.get())
154156
}
155157

156158
// graphql-java only waits on a CompletionStage returned by a data fetcher, so any other future, or a future

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import org.junit.Test
4343
import java.util.*
4444
import java.util.concurrent.CompletableFuture
4545
import java.util.concurrent.CompletableFuture.completedFuture
46+
import java.util.concurrent.CompletionStage
4647
import java.util.concurrent.Future
4748
import java.util.concurrent.FutureTask
4849

@@ -171,6 +172,19 @@ class NestedGenericWrapperTest {
171172
@Test
172173
fun `hands a future graphql-java can't wait on to its own wrapper`() = assertResolves("listOfWrappedPending", ab)
173174

175+
@Test
176+
fun `waits on a future in a wrapper whose transformer returns a future`() = assertResolves("promisedFuture", mapOf("data" to "a"))
177+
178+
@Test
179+
fun `waits on a scalar future in a wrapper whose transformer returns a future`() = assertResolves("promisedString", "a", selection = "")
180+
181+
@Test
182+
fun `waits on futures in wrappers whose transformer returns a future in a list`() = assertResolves("listOfPromisedFutures", ab)
183+
184+
@Test
185+
fun `resolves a union member that implements Future in a list of wrappers whose transformer returns a future`() =
186+
assertResolves("listOfPromisedItems", ab, selection = "{ ... on Payload { data } ... on FutureItem { data } }")
187+
174188
@Test
175189
fun `unwraps the wrappers a schemaWrapper introduces`() = assertResolves("batchOfPayloads", ab)
176190

@@ -368,6 +382,10 @@ class NestedGenericWrapperTest {
368382
plainList: [Payload!]!
369383
futureOfList: [Payload!]!
370384
wrappedFuture: Payload!
385+
promisedFuture: Payload!
386+
promisedString: String
387+
listOfPromisedFutures: [Payload!]!
388+
listOfPromisedItems: [Item!]!
371389
${if (scalars.isEmpty()) "" else "json: [JSON] futureJson: JSON jsonFutures: [JSON] wrappedJsonOfAny: [JSON]"}
372390
}
373391
@@ -386,7 +404,7 @@ class NestedGenericWrapperTest {
386404
.resolvers(query)
387405
.dictionary(FutureItem::class)
388406
.scalars(*scalars)
389-
.options(SchemaParserOptions.newOptions().genericWrappers(tryWrapper, pageWrapper, batchWrapper, boxWrapper, specialBoxWrapper, laterWrapper, pendingWrapper).build())
407+
.options(SchemaParserOptions.newOptions().genericWrappers(tryWrapper, pageWrapper, batchWrapper, boxWrapper, specialBoxWrapper, laterWrapper, pendingWrapper, promisedWrapper).build())
390408
.build()
391409
.makeExecutableSchema()
392410

@@ -496,6 +514,10 @@ class NestedGenericWrapperTest {
496514
fun wrappedJsonOfAny(): List<Try<Any>> = listOf(Try.success(FutureLeaf("f")))
497515
fun batchOfPayloads(): Batch<Payload> = Batch(listOfFutures())
498516
fun listOfBoxes(): List<Box<Payload>> = listOf(Box(Payload("a")), SpecialBox(Payload("b")))
517+
fun promisedFuture(): Promised<CompletableFuture<Payload>> = Promised(CompletableFuture.supplyAsync { completedFuture(Payload("a")) })
518+
fun promisedString(): Promised<CompletionStage<String>> = Promised(CompletableFuture.supplyAsync { completedFuture("a") })
519+
fun listOfPromisedFutures(): List<Promised<CompletableFuture<Payload>>> = listOfFutures().map { Promised(CompletableFuture.supplyAsync { it }) }
520+
fun listOfPromisedItems(): List<Promised<Any>> = listOf(Promised(completedFuture(Payload("a"))), Promised(CompletableFuture.supplyAsync { FutureItem("b") }))
499521
}
500522

501523
class Batch<T>(val items: List<CompletableFuture<T>>)
@@ -508,6 +530,8 @@ class NestedGenericWrapperTest {
508530

509531
class Pending<T>(val value: T) : Future<T> by completedFuture(value)
510532

533+
class Promised<T>(val future: CompletableFuture<T>)
534+
511535
companion object {
512536
private val tryWrapper = GenericWrapper.withTransformer(Try::class, 0, { wrapper -> wrapper.get() })
513537
private val pageWrapper = GenericWrapper.listCollectionWithTransformer(Page::class, 0, { page -> page.content })
@@ -518,6 +542,7 @@ class NestedGenericWrapperTest {
518542
private val specialBoxWrapper = GenericWrapper.withTransformer(SpecialBox::class, 0, { _ -> throw IllegalStateException("picked by the value's class") })
519543
private val laterWrapper = GenericWrapper.withTransformer(Later::class, 0, { later -> FutureTask { later.value }.apply { run() } })
520544
private val pendingWrapper = GenericWrapper.withTransformer(Pending::class, 0, { pending -> completedFuture(pending.value) })
545+
private val promisedWrapper = GenericWrapper.withCompletionStageTransformer(Promised::class, 0, Promised<*>::future)
521546
private val payloadType = GraphQLObjectType.newObject()
522547
.name("Payload")
523548
.field(GraphQLFieldDefinition.newFieldDefinition().name("data").type(GraphQLNonNull.nonNull(Scalars.GraphQLString)))

0 commit comments

Comments
 (0)