Skip to content

Commit d302771

Browse files
authored
Merge pull request #797 from graphql-java-kickstart/334-allow-future-return-type
Allow future return type for subscription data fetcher
2 parents 9a61df2 + 5bd12de commit d302771

File tree

4 files changed

+106
-12
lines changed

4 files changed

+106
-12
lines changed

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

+13-5
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ import org.apache.commons.lang3.ClassUtils
1515
import org.apache.commons.lang3.reflect.FieldUtils
1616
import org.reactivestreams.Publisher
1717
import org.slf4j.LoggerFactory
18-
import java.lang.reflect.AccessibleObject
19-
import java.lang.reflect.Method
20-
import java.lang.reflect.Modifier
21-
import java.lang.reflect.Type
18+
import java.lang.reflect.*
19+
import java.util.concurrent.CompletableFuture
2220
import kotlin.reflect.full.valueParameters
2321
import kotlin.reflect.jvm.javaType
2422
import kotlin.reflect.jvm.kotlinFunction
@@ -131,7 +129,17 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
131129
}
132130

133131
private fun resolverMethodReturnsPublisher(method: Method) =
134-
method.returnType.isAssignableFrom(Publisher::class.java) || receiveChannelToPublisherWrapper(method)
132+
method.returnType.isAssignableFrom(Publisher::class.java)
133+
|| resolverMethodReturnsPublisherFuture(method)
134+
|| receiveChannelToPublisherWrapper(method)
135+
136+
private fun resolverMethodReturnsPublisherFuture(method: Method) =
137+
method.returnType.isAssignableFrom(CompletableFuture::class.java)
138+
&& method.genericReturnType is ParameterizedType
139+
&& (method.genericReturnType as ParameterizedType).actualTypeArguments
140+
.any {
141+
it is ParameterizedType && it.unwrap().isAssignableFrom(Publisher::class.java)
142+
}
135143

136144
private fun receiveChannelToPublisherWrapper(method: Method) =
137145
method.returnType.isAssignableFrom(ReceiveChannel::class.java)

src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt

+9-1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ type Mutation {
123123
124124
type Subscription {
125125
onItemCreated: Item!
126+
onItemCreatedFuture: Item!
126127
onItemCreatedCoroutineChannel: Item!
127128
onItemCreatedCoroutineChannelAndSuspendFunction: Item!
128129
}
@@ -373,7 +374,6 @@ class Subscription : GraphQLSubscriptionResolver {
373374
fun onItemCreated(env: DataFetchingEnvironment) =
374375
Publisher<Item> { subscriber ->
375376
subscriber.onNext(env.graphQlContext["newItem"])
376-
// subscriber.onComplete()
377377
}
378378

379379
fun onItemCreatedCoroutineChannel(env: DataFetchingEnvironment): ReceiveChannel<Item> {
@@ -382,6 +382,14 @@ class Subscription : GraphQLSubscriptionResolver {
382382
return channel
383383
}
384384

385+
fun onItemCreatedFuture(env: DataFetchingEnvironment): CompletableFuture<Publisher<Item>> {
386+
return CompletableFuture.supplyAsync {
387+
Publisher<Item> { subscriber ->
388+
subscriber.onNext(env.graphQlContext["newItem"])
389+
}
390+
}
391+
}
392+
385393
suspend fun onItemCreatedCoroutineChannelAndSuspendFunction(env: DataFetchingEnvironment): ReceiveChannel<Item> {
386394
return coroutineScope {
387395
val channel = Channel<Item>(1)

src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt

+38-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package graphql.kickstart.tools
22

3-
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
43
import graphql.*
54
import graphql.execution.AsyncExecutionStrategy
65
import graphql.schema.*
@@ -97,6 +96,44 @@ class EndToEndTest {
9796
assert(result.errors.isEmpty())
9897
assertEquals(returnedItem?.get("onItemCreated"), mapOf("id" to 1))
9998
}
99+
100+
@Test
101+
fun `generated schema should execute the subscription query future`() {
102+
val newItem = Item(1, "item", Type.TYPE_1, UUID.randomUUID(), listOf())
103+
var returnedItem: Map<String, Map<String, Any>>? = null
104+
105+
val closure = {
106+
"""
107+
subscription {
108+
onItemCreatedFuture {
109+
id
110+
}
111+
}
112+
"""
113+
}
114+
115+
val result = gql.execute(ExecutionInput.newExecutionInput()
116+
.query(closure.invoke())
117+
.graphQLContext(mapOf("newItem" to newItem))
118+
.variables(mapOf()))
119+
120+
val data = result.getData() as Publisher<ExecutionResult>
121+
val latch = CountDownLatch(1)
122+
data.subscribe(object : Subscriber<ExecutionResult> {
123+
override fun onNext(item: ExecutionResult?) {
124+
returnedItem = item?.getData()
125+
latch.countDown()
126+
}
127+
128+
override fun onError(throwable: Throwable?) {}
129+
override fun onComplete() {}
130+
override fun onSubscribe(p0: Subscription?) {}
131+
})
132+
latch.await(3, TimeUnit.SECONDS)
133+
134+
assert(result.errors.isEmpty())
135+
assertEquals(returnedItem?.get("onItemCreatedFuture"), mapOf("id" to 1))
136+
}
100137

101138
@Test
102139
fun `generated schema should handle interface types`() {

src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt

+46-5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.junit.Before
1010
import org.junit.Test
1111
import org.springframework.aop.framework.ProxyFactory
1212
import java.io.FileNotFoundException
13+
import java.util.concurrent.CompletableFuture.completedFuture
1314
import java.util.concurrent.Future
1415

1516
@OptIn(ExperimentalCoroutinesApi::class)
@@ -665,6 +666,10 @@ class SchemaParserTest {
665666

666667
@Test
667668
fun `parser should verify subscription resolver return type`() {
669+
class Subscription : GraphQLSubscriptionResolver {
670+
fun onItemCreated(env: DataFetchingEnvironment) = env.hashCode()
671+
}
672+
668673
val error = assertThrows(FieldResolverError::class.java) {
669674
SchemaParser.newParser()
670675
.schemaString(
@@ -689,17 +694,53 @@ class SchemaParserTest {
689694
val expected = """
690695
No method or field found as defined in schema <unknown>:3 with any of the following signatures (with or without one of [interface graphql.schema.DataFetchingEnvironment, class graphql.GraphQLContext] as the last argument), in priority order:
691696
692-
graphql.kickstart.tools.SchemaParserTest${"$"}Subscription.onItemCreated()
693-
graphql.kickstart.tools.SchemaParserTest${"$"}Subscription.getOnItemCreated()
694-
graphql.kickstart.tools.SchemaParserTest${"$"}Subscription.onItemCreated
697+
graphql.kickstart.tools.SchemaParserTest${"$"}parser should verify subscription resolver return type${"$"}Subscription.onItemCreated()
698+
graphql.kickstart.tools.SchemaParserTest${"$"}parser should verify subscription resolver return type${"$"}Subscription.getOnItemCreated()
699+
graphql.kickstart.tools.SchemaParserTest${"$"}parser should verify subscription resolver return type${"$"}Subscription.onItemCreated
695700
696701
Note that a Subscription data fetcher must return a Publisher of events
697702
""".trimIndent()
698703

699704
assertEquals(error.message, expected)
700705
}
701706

702-
class Subscription : GraphQLSubscriptionResolver {
703-
fun onItemCreated(env: DataFetchingEnvironment) = env.hashCode()
707+
@Test
708+
fun `parser should verify subscription resolver generic future return type`() {
709+
class Subscription : GraphQLSubscriptionResolver {
710+
fun onItemCreated(env: DataFetchingEnvironment) = completedFuture(env.hashCode())
711+
}
712+
713+
val error = assertThrows(FieldResolverError::class.java) {
714+
SchemaParser.newParser()
715+
.schemaString(
716+
"""
717+
type Subscription {
718+
onItemCreated: Int!
719+
}
720+
721+
type Query {
722+
test: String
723+
}
724+
"""
725+
)
726+
.resolvers(
727+
Subscription(),
728+
object : GraphQLQueryResolver { fun test() = "test" }
729+
)
730+
.build()
731+
.makeExecutableSchema()
732+
}
733+
734+
val expected = """
735+
No method or field found as defined in schema <unknown>:3 with any of the following signatures (with or without one of [interface graphql.schema.DataFetchingEnvironment, class graphql.GraphQLContext] as the last argument), in priority order:
736+
737+
graphql.kickstart.tools.SchemaParserTest${"$"}parser should verify subscription resolver generic future return type${"$"}Subscription.onItemCreated()
738+
graphql.kickstart.tools.SchemaParserTest${"$"}parser should verify subscription resolver generic future return type${"$"}Subscription.getOnItemCreated()
739+
graphql.kickstart.tools.SchemaParserTest${"$"}parser should verify subscription resolver generic future return type${"$"}Subscription.onItemCreated
740+
741+
Note that a Subscription data fetcher must return a Publisher of events
742+
""".trimIndent()
743+
744+
assertEquals(error.message, expected)
704745
}
705746
}

0 commit comments

Comments
 (0)