Skip to content

Commit 1b314a3

Browse files
authored
Merge pull request #940 from graphql-java-kickstart/bugfix/optional-data-fetcher-result
Unwrap a DataFetcherResult inside an Optional
2 parents 3f022b9 + 2c00200 commit 1b314a3

3 files changed

Lines changed: 109 additions & 39 deletions

File tree

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

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -60,36 +60,36 @@ internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeD
6060
private fun unwrap(graphQLType: GraphQLLangType, javaType: JavaType, root: Boolean, unwrapped: MutableList<Unwrapped>): Unwrapping {
6161
val onUnwrap = { wrapper: GenericWrapper -> unwrapped += Unwrapped.Wrapper(wrapper) }
6262
var realType = typeResolver.resolve(javaType, onUnwrap)
63-
64-
if (realType is ParameterizedType && realType.isSubtypeOf(DataFetcherResult::class.java)) {
65-
if (location != Location.RETURN_TYPE) {
66-
throw error("${DataFetcherResult::class.java.name} can only be used as a return type")
67-
}
68-
69-
unwrapped += Unwrapped.Data
70-
realType = typeResolver.resolve(realType.actualTypeArguments.first(), onUnwrap)
71-
72-
if (realType is ParameterizedType && realType.isSubtypeOf(DataFetcherResult::class.java)) {
73-
throw error("${DataFetcherResult::class.java.name} cannot be nested within itself")
74-
}
75-
}
76-
63+
var data = false
7764
var optional = false
7865

79-
// Handle jdk8 Optionals
80-
if (realType is ParameterizedType && realType.isSubtypeOf(Optional::class.java)) {
81-
optional = true
82-
83-
if (location == Location.RETURN_TYPE && !root) {
84-
throw error("${Optional::class.java.name} can only be used at the top level of a return type")
85-
}
66+
while (realType is ParameterizedType) {
67+
when {
68+
realType.isSubtypeOf(DataFetcherResult::class.java) -> {
69+
if (location != Location.RETURN_TYPE) {
70+
throw error("${DataFetcherResult::class.java.name} can only be used as a return type")
71+
}
72+
if (data) {
73+
throw error("${DataFetcherResult::class.java.name} cannot be nested within itself")
74+
}
75+
data = true
76+
unwrapped += Unwrapped.Data
77+
}
8678

87-
unwrapped += Unwrapped.Present
88-
realType = typeResolver.resolve(realType.actualTypeArguments.first(), onUnwrap)
79+
realType.isSubtypeOf(Optional::class.java) -> {
80+
if (location == Location.RETURN_TYPE && !root) {
81+
throw error("${Optional::class.java.name} can only be used at the top level of a return type")
82+
}
83+
if (optional) {
84+
throw error("${Optional::class.java.name} cannot be nested within itself")
85+
}
86+
optional = true
87+
unwrapped += Unwrapped.Present
88+
}
8989

90-
if (realType is ParameterizedType && realType.isSubtypeOf(Optional::class.java)) {
91-
throw error("${Optional::class.java.name} cannot be nested within itself")
90+
else -> break
9291
}
92+
realType = typeResolver.resolve(realType.actualTypeArguments.first(), onUnwrap)
9393
}
9494

9595
// Match graphql type to java type.

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

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,23 +88,29 @@ private class DataOf(private val data: Step) : Step {
8888
}
8989
}
9090

91-
private class ValueOf(private val present: Step) : Step {
92-
override fun unwrap(value: Any?, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? =
93-
if (value is Optional<*>) present.unwrap(value.orElse(null), environmentSupplier) else value
91+
private class ValueOf(private val present: Step?) : Step {
92+
override fun unwrap(value: Any?, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? = when {
93+
value !is Optional<*> -> value
94+
present == null -> value.orElse(null)
95+
else -> present.unwrap(value.orElse(null), environmentSupplier)
96+
}
9497
}
9598

9699
private val Unwrapped.Wrapper.isFuture
97100
get() = Future::class.java.isAssignableFrom(wrapper.type) || CompletionStage::class.java.isAssignableFrom(wrapper.type)
98101

99-
private fun steps(unwrapped: List<Unwrapped>, awaits: Awaitable, peels: Awaitable): Step? =
100-
unwrapped.foldRight(null as Step?) { it, next ->
101-
when (it) {
102-
is Unwrapped.Wrapper -> if (it.isFuture) Await(it.wrapper, awaits, next) else Peel(it.wrapper, peels, next)
103-
Unwrapped.Data -> next?.let(::DataOf)
104-
Unwrapped.Present -> next?.let(::ValueOf)
105-
Unwrapped.Elements -> next?.let(::EachElement)
106-
}
102+
private fun steps(unwrapped: List<Unwrapped>, awaits: Awaitable, peels: Awaitable): Step? {
103+
val outermost = unwrapped.firstOrNull() ?: return null
104+
val inner = unwrapped.subList(1, unwrapped.size)
105+
val next = steps(inner, awaits, peels)
106+
return when (outermost) {
107+
is Unwrapped.Wrapper -> if (outermost.isFuture) Await(outermost.wrapper, awaits, next) else Peel(outermost.wrapper, peels, next)
108+
Unwrapped.Data -> next?.let(::DataOf)
109+
// graphql-java reads a DataFetcherResult and then an Optional in its data, so an Optional around one is ours to unwrap
110+
Unwrapped.Present -> if (next != null || inner.firstOrNull() == Unwrapped.Data) ValueOf(next) else null
111+
Unwrapped.Elements -> next?.let(::EachElement)
107112
}
113+
}
108114

109115
private fun Any?.thenUnlessNextAwaits(next: Step?, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? =
110116
if (next is Await) next.unwrap(this, environmentSupplier) else then(next, environmentSupplier)

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

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,25 @@ class NestedGenericWrapperTest {
9393
@Test
9494
fun `unwraps wrappers in a list in an Optional`() = assertResolves("optionalListOfWrapped", ab)
9595

96+
@Test
97+
fun `resolves a DataFetcherResult in an Optional and keeps its errors`() {
98+
val result = execute("optionalResult")
99+
100+
assertEquals(result.errors.map { it.message }, listOf("partial"))
101+
assertEquals(result.getData(), mapOf("optionalResult" to mapOf("data" to "a")))
102+
}
103+
104+
@Test
105+
fun `resolves an empty Optional of a DataFetcherResult to null`() = assertResolves("emptyOptionalResult", null)
106+
107+
@Test
108+
fun `unwraps wrappers in a list in a DataFetcherResult in an Optional and keeps its errors`() {
109+
val result = execute("optionalResultOfListOfWrapped")
110+
111+
assertEquals(result.errors.map { it.message }, listOf("partial"))
112+
assertEquals(result.getData(), mapOf("optionalResultOfListOfWrapped" to ab))
113+
}
114+
96115
@Test
97116
fun `unwraps wrappers in a list from a suspend function`() = assertResolves("suspendListOfWrapped", ab)
98117

@@ -178,6 +197,7 @@ class NestedGenericWrapperTest {
178197
assertSame(fetch(schema, "plainList"), query.plainList)
179198
assertSame(fetch(schema, "futureOfList"), query.futureOfList)
180199
assertSame(fetch(schema, "wrappedFuture"), query.wrappedFuture.get())
200+
assertSame(fetch(schema, "optionalPayload"), query.optionalPayload)
181201
}
182202

183203
@Test
@@ -202,18 +222,22 @@ class NestedGenericWrapperTest {
202222
val shapes = (1..4).fold(listOf(listOf<Class<*>>())) { shapes, _ -> shapes + shapes.filter { it.size == shapes.last().size }.flatMap { shape -> containers.map { shape + it } } }
203223

204224
val accepted = graphQLTypes.flatMap { graphQLType ->
205-
shapes.flatMap { shape -> listOf(false, true).map { shape to javaType(shape, leaf(graphQLType).javaClass, wildcardElements = it) } }.mapNotNull { (shape, javaType) ->
225+
val shapesOfTheSameListDepth = shapes.filter { shape -> shape.count { it == List::class.java || it == Page::class.java } == graphQLType.listDepth() }
226+
shapesOfTheSameListDepth.flatMap { shape -> listOf(false, true).map { shape to javaType(shape, leaf(graphQLType).javaClass, wildcardElements = it) } }.mapNotNull { (shape, javaType) ->
206227
val potentialMatch = TypeClassMatcher.PotentialMatch.returnValue(graphQLType, javaType, typeResolver, ScanDictionary.ReturnValueReference(method))
207-
val match = try {
228+
try {
208229
matcher.match(potentialMatch)
230+
shape to potentialMatch
209231
} catch (e: SchemaClassScannerError) {
210232
null
211233
}
212-
(shape to potentialMatch).takeIf { match != null && potentialMatch.unwrapping.javaType == leaf(graphQLType).javaClass }
213234
}
214235
}
215236
val failures = accepted.mapNotNull { (shape, potentialMatch) ->
216237
val graphQLType = potentialMatch.graphQLType
238+
if (potentialMatch.unwrapping.javaType != leaf(graphQLType).javaClass) {
239+
return@mapNotNull "${TypeUtils.toString(potentialMatch.javaType)} as ${AstPrinter.printAst(graphQLType)}: bound to ${TypeUtils.toString(potentialMatch.unwrapping.javaType)}"
240+
}
217241
val unwrapper = ReturnValueUnwrapper.create(
218242
method, potentialMatch.unwrapping, TypeUtils.getRawType(typeResolver.resolve(potentialMatch.javaType), null), isSuspendFunction = false, isSubscription = false, options.genericWrappers
219243
)
@@ -232,6 +256,38 @@ class NestedGenericWrapperTest {
232256
assertEquals(failures, listOf<String>())
233257
}
234258

259+
@Test
260+
fun `rejects a DataFetcherResult or an Optional twice in one value`() {
261+
val typeResolver = GenericTypeResolver(Query::class.java, SchemaParserOptions.newOptions().build())
262+
val matcher = TypeClassMatcher(mapOf("Payload" to ObjectTypeDefinition("Payload")))
263+
val method = Query::class.java.getMethod("listOfWrapped")
264+
val result = DataFetcherResult::class.java
265+
val optional = Optional::class.java
266+
val rejections = listOf(
267+
listOf(optional, result, result) to "${result.name} cannot be nested within itself",
268+
listOf(result, optional, result) to "${result.name} cannot be nested within itself",
269+
listOf(optional, result, optional) to "${optional.name} cannot be nested within itself",
270+
)
271+
272+
val messages = rejections.map { (shape, _) ->
273+
val potentialMatch = TypeClassMatcher.PotentialMatch.returnValue(TypeName("Payload"), javaType(shape, Payload::class.java, wildcardElements = false), typeResolver, ScanDictionary.ReturnValueReference(method))
274+
try {
275+
matcher.match(potentialMatch)
276+
"accepted"
277+
} catch (e: SchemaClassScannerError) {
278+
e.message.orEmpty().substringAfterLast("): ")
279+
}
280+
}
281+
282+
assertEquals(messages, rejections.map { it.second })
283+
}
284+
285+
private fun Type<*>.listDepth(): Int = when (this) {
286+
is ListType -> 1 + type.listDepth()
287+
is NonNullType -> type.listDepth()
288+
else -> 0
289+
}
290+
235291
private fun leaf(graphQLType: Type<*>): Any = if (graphQLType.unwrap().name == "Leaf") FutureLeaf("a") else Payload("a")
236292

237293
private fun javaType(shape: List<Class<*>>, leafClass: Class<*>, wildcardElements: Boolean): java.lang.reflect.Type =
@@ -284,6 +340,10 @@ class NestedGenericWrapperTest {
284340
pageOfWrapped: [Payload!]!
285341
resultOfListOfWrapped: [Payload!]!
286342
listOfResultsOfWrapped: [Payload!]!
343+
optionalResult: Payload
344+
emptyOptionalResult: Payload
345+
optionalResultOfListOfWrapped: [Payload!]
346+
optionalPayload: Payload
287347
optionalListOfWrapped: [Payload!]
288348
suspendListOfWrapped: [Payload!]!
289349
suspendListOfFutures: [Payload!]!
@@ -387,6 +447,7 @@ class NestedGenericWrapperTest {
387447
val plainList = listOf(Payload("a"))
388448
val futureOfList: CompletableFuture<List<Payload>> = completedFuture(plainList)
389449
val wrappedFuture: Try<CompletableFuture<Payload>> = Try.success(completedFuture(Payload("a")))
450+
val optionalPayload: Optional<Payload> = Optional.of(Payload("a"))
390451

391452
private fun <T : Any> result(data: T, vararg errors: GraphQLError): DataFetcherResult<T> = DataFetcherResult.newResult<T>().data(data).errors(errors.toList()).build()
392453

@@ -406,6 +467,9 @@ class NestedGenericWrapperTest {
406467
fun resultOfListOfWrapped(): DataFetcherResult<List<Try<Payload>>> = result(listOfWrapped(), GraphQLError.newError().message("partial").build())
407468
fun listOfResultsOfWrapped(): List<DataFetcherResult<Try<Payload>>> = listOfWrapped().map { result(it) }
408469
fun optionalListOfWrapped(): Optional<List<Try<Payload>>> = Optional.of(listOfWrapped())
470+
fun optionalResult(): Optional<DataFetcherResult<Payload>> = Optional.of(result(Payload("a"), GraphQLError.newError().message("partial").build()))
471+
fun emptyOptionalResult(): Optional<DataFetcherResult<Payload>> = Optional.empty()
472+
fun optionalResultOfListOfWrapped(): Optional<DataFetcherResult<List<Try<Payload>>>> = Optional.of(resultOfListOfWrapped())
409473
suspend fun suspendListOfWrapped(): List<Try<Payload>> = listOfWrapped()
410474
suspend fun suspendListOfFutures(): List<CompletableFuture<Payload>> = listOfFutures()
411475
suspend fun suspendNullableListOfFutures(): List<CompletableFuture<Payload>> = listOfFutures()

0 commit comments

Comments
 (0)