Skip to content

Commit 2c96c35

Browse files
authored
Merge pull request #201 from varahash/feature/kotlin-coroutines-support
Kotlin coroutines support
2 parents d33cc47 + cfc02dc commit 2c96c35

File tree

7 files changed

+308
-23
lines changed

7 files changed

+308
-23
lines changed

pom.xml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<properties>
1515
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1616
<java.version>1.8</java.version>
17-
<kotlin.version>1.2.71</kotlin.version>
17+
<kotlin.version>1.3.0</kotlin.version>
1818
<jackson.version>2.9.6</jackson.version>
1919

2020
<maven.compiler.source>${java.version}</maven.compiler.source>
@@ -29,6 +29,21 @@
2929
<artifactId>kotlin-stdlib</artifactId>
3030
<version>${kotlin.version}</version>
3131
</dependency>
32+
<dependency>
33+
<groupId>org.jetbrains.kotlin</groupId>
34+
<artifactId>kotlin-reflect</artifactId>
35+
<version>${kotlin.version}</version>
36+
</dependency>
37+
<dependency>
38+
<groupId>org.jetbrains.kotlinx</groupId>
39+
<artifactId>kotlinx-coroutines-jdk8</artifactId>
40+
<version>1.0.0</version>
41+
</dependency>
42+
<dependency>
43+
<groupId>org.jetbrains.kotlinx</groupId>
44+
<artifactId>kotlinx-coroutines-reactive</artifactId>
45+
<version>1.0.0</version>
46+
</dependency>
3247
<dependency>
3348
<groupId>com.graphql-java</groupId>
3449
<artifactId>graphql-java</artifactId>
@@ -110,6 +125,12 @@
110125
<version>2.1</version>
111126
<scope>test</scope>
112127
</dependency>
128+
<dependency>
129+
<groupId>org.reactivestreams</groupId>
130+
<artifactId>reactive-streams-tck</artifactId>
131+
<version>1.0.2</version>
132+
<scope>test</scope>
133+
</dependency>
113134
</dependencies>
114135

115136
<build>

src/main/kotlin/com/coxautodev/graphql/tools/FieldResolverScanner.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import org.apache.commons.lang3.reflect.FieldUtils
99
import org.slf4j.LoggerFactory
1010
import java.lang.reflect.Modifier
1111
import java.lang.reflect.ParameterizedType
12+
import kotlin.reflect.full.valueParameters
13+
import kotlin.reflect.jvm.javaType
14+
import kotlin.reflect.jvm.kotlinFunction
1215

1316
/**
1417
* @author Andrew Potter
@@ -112,7 +115,11 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
112115
true
113116
}
114117

115-
val correctParameterCount = method.parameterCount == requiredCount || (method.parameterCount == (requiredCount + 1) && allowedLastArgumentTypes.contains(method.parameterTypes.last()))
118+
val methodParameterCount = method.kotlinFunction?.valueParameters?.size ?: method.parameterCount
119+
val methodLastParameter = method.kotlinFunction?.valueParameters?.lastOrNull()?.type?.javaType ?: method.parameterTypes.lastOrNull()
120+
121+
val correctParameterCount = methodParameterCount == requiredCount ||
122+
(methodParameterCount == (requiredCount + 1) && allowedLastArgumentTypes.contains(methodLastParameter))
116123
return correctParameterCount && appropriateFirstParameter
117124
}
118125

src/main/kotlin/com/coxautodev/graphql/tools/MethodFieldResolver.kt

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ import graphql.language.FieldDefinition
88
import graphql.language.NonNullType
99
import graphql.schema.DataFetcher
1010
import graphql.schema.DataFetchingEnvironment
11+
import kotlinx.coroutines.GlobalScope
12+
import kotlinx.coroutines.future.future
1113
import java.lang.reflect.Method
12-
import java.lang.reflect.ParameterizedType
13-
import java.lang.reflect.TypeVariable
1414
import java.util.*
15+
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
16+
import kotlin.reflect.full.valueParameters
17+
import kotlin.reflect.jvm.javaType
18+
import kotlin.reflect.jvm.kotlinFunction
1519

1620
/**
1721
* @author Andrew Potter
@@ -31,7 +35,7 @@ internal class MethodFieldResolver(field: FieldDefinition, search: FieldResolver
3135
}
3236
}
3337

34-
private val additionalLastArgument = method.parameterCount == (field.inputValueDefinitions.size + getIndexOffset() + 1)
38+
private val additionalLastArgument = method.kotlinFunction?.valueParameters?.size ?: method.parameterCount == (field.inputValueDefinitions.size + getIndexOffset() + 1)
3539

3640
override fun createDataFetcher(): DataFetcher<*> {
3741
val batched = isBatched(method, search)
@@ -99,7 +103,7 @@ internal class MethodFieldResolver(field: FieldDefinition, search: FieldResolver
99103

100104
override fun scanForMatches(): List<TypeClassMatcher.PotentialMatch> {
101105
val batched = isBatched(method, search)
102-
val unwrappedGenericType = genericType.unwrapGenericType(method.genericReturnType)
106+
val unwrappedGenericType = genericType.unwrapGenericType(method.kotlinFunction?.returnType?.javaType ?: method.returnType)
103107
val returnValueMatch = TypeClassMatcher.PotentialMatch.returnValue(field.type, unwrappedGenericType, genericType, SchemaClassScanner.ReturnValueReference(method), batched)
104108

105109
return field.inputValueDefinitions.mapIndexed { i, inputDefinition ->
@@ -136,6 +140,7 @@ open class MethodFieldResolverDataFetcher(private val sourceResolver: SourceReso
136140
// Convert to reflactasm reflection
137141
private val methodAccess = MethodAccess.get(method.declaringClass)!!
138142
private val methodIndex = methodAccess.getIndex(method.name, *method.parameterTypes)
143+
private val isSuspendFunction = method.kotlinFunction?.isSuspend == true
139144

140145
private class CompareGenericWrappers {
141146
companion object : Comparator<GenericWrapper> {
@@ -149,23 +154,25 @@ open class MethodFieldResolverDataFetcher(private val sourceResolver: SourceReso
149154
override fun get(environment: DataFetchingEnvironment): Any? {
150155
val source = sourceResolver(environment)
151156
val args = this.args.map { it(environment) }.toTypedArray()
152-
val result = methodAccess.invoke(source, methodIndex, *args)
153-
return if (result == null) {
154-
result
155-
} else {
156-
val wrapper = options.genericWrappers
157-
.asSequence()
158-
.filter { it.type.isInstance(result) }
159-
.sortedWith(CompareGenericWrappers)
160-
.firstOrNull()
161-
if (wrapper == null) {
162-
result
163-
} else {
164-
wrapper.transformer.invoke(result, environment)
157+
158+
return if (isSuspendFunction) {
159+
GlobalScope.future(options.coroutineContext) {
160+
methodAccess.invokeSuspend(source, methodIndex, args)?.transformWithGenericWrapper(environment)
165161
}
162+
} else {
163+
methodAccess.invoke(source, methodIndex, *args)?.transformWithGenericWrapper(environment)
166164
}
167165
}
168166

167+
private fun Any.transformWithGenericWrapper(environment: DataFetchingEnvironment): Any? {
168+
return options.genericWrappers
169+
.asSequence()
170+
.filter { it.type.isInstance(this) }
171+
.sortedWith(CompareGenericWrappers)
172+
.firstOrNull()
173+
?.transformer?.invoke(this, environment) ?: this
174+
}
175+
169176
/**
170177
* Function that return the object used to fetch the data
171178
* It can be a DataFetcher or an entity
@@ -176,6 +183,12 @@ open class MethodFieldResolverDataFetcher(private val sourceResolver: SourceReso
176183
}
177184
}
178185

186+
private suspend inline fun MethodAccess.invokeSuspend(target: Any, methodIndex: Int, args: Array<Any?>): Any? {
187+
return suspendCoroutineUninterceptedOrReturn { continuation ->
188+
invoke(target, methodIndex, *args + continuation)
189+
}
190+
}
191+
179192
class BatchedMethodFieldResolverDataFetcher(sourceResolver: SourceResolver, method: Method, args: List<ArgumentPlaceholder>, options: SchemaParserOptions) : MethodFieldResolverDataFetcher(sourceResolver, method, args, options) {
180193
@Batched
181194
override fun get(environment: DataFetchingEnvironment) = super.get(environment)

src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ import graphql.language.Document
88
import graphql.parser.Parser
99
import graphql.schema.DataFetchingEnvironment
1010
import graphql.schema.GraphQLScalarType
11+
import kotlinx.coroutines.Dispatchers
12+
import kotlinx.coroutines.GlobalScope
13+
import kotlinx.coroutines.channels.ReceiveChannel
14+
import kotlinx.coroutines.reactive.publish
1115
import org.antlr.v4.runtime.RecognitionException
1216
import org.antlr.v4.runtime.misc.ParseCancellationException
1317
import org.reactivestreams.Publisher
1418
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
1519
import java.util.concurrent.CompletableFuture
1620
import java.util.concurrent.CompletionStage
1721
import java.util.concurrent.Future
22+
import kotlin.coroutines.CoroutineContext
1823
import kotlin.reflect.KClass
1924

2025
/**
@@ -247,7 +252,16 @@ class SchemaParserDictionary {
247252
}
248253
}
249254

250-
data class SchemaParserOptions internal constructor(val contextClass: Class<*>?, val genericWrappers: List<GenericWrapper>, val allowUnimplementedResolvers: Boolean, val objectMapperProvider: PerFieldObjectMapperProvider, val proxyHandlers: List<ProxyHandler>, val preferGraphQLResolver: Boolean, val introspectionEnabled: Boolean) {
255+
data class SchemaParserOptions internal constructor(
256+
val contextClass: Class<*>?,
257+
val genericWrappers: List<GenericWrapper>,
258+
val allowUnimplementedResolvers: Boolean,
259+
val objectMapperProvider: PerFieldObjectMapperProvider,
260+
val proxyHandlers: List<ProxyHandler>,
261+
val preferGraphQLResolver: Boolean,
262+
val introspectionEnabled: Boolean,
263+
val coroutineContext: CoroutineContext
264+
) {
251265
companion object {
252266
@JvmStatic
253267
fun newOptions() = Builder()
@@ -265,6 +279,7 @@ data class SchemaParserOptions internal constructor(val contextClass: Class<*>?,
265279
private val proxyHandlers: MutableList<ProxyHandler> = mutableListOf(Spring4AopProxyHandler(), GuiceAopProxyHandler(), JavassistProxyHandler())
266280
private var preferGraphQLResolver = false
267281
private var introspectionEnabled = true
282+
private var coroutineContext: CoroutineContext? = null
268283

269284
fun contextClass(contextClass: Class<*>) = this.apply {
270285
this.contextClass = contextClass
@@ -314,19 +329,36 @@ data class SchemaParserOptions internal constructor(val contextClass: Class<*>?,
314329
this.introspectionEnabled = introspectionEnabled
315330
}
316331

332+
fun coroutineContext(context: CoroutineContext) = this.apply {
333+
this.coroutineContext = context
334+
}
335+
317336
fun build(): SchemaParserOptions {
337+
val coroutineContext = coroutineContext ?: Dispatchers.Default
318338
val wrappers = if (useDefaultGenericWrappers) {
319339
genericWrappers + listOf(
320340
GenericWrapper(Future::class, 0),
321341
GenericWrapper(CompletableFuture::class, 0),
322342
GenericWrapper(CompletionStage::class, 0),
323-
GenericWrapper(Publisher::class, 0)
343+
GenericWrapper(Publisher::class, 0),
344+
GenericWrapper.withTransformer(ReceiveChannel::class, 0, { receiveChannel ->
345+
GlobalScope.publish(coroutineContext) {
346+
try {
347+
for (item in receiveChannel) {
348+
send(item)
349+
}
350+
} finally {
351+
receiveChannel.cancel()
352+
}
353+
}
354+
})
324355
)
325356
} else {
326357
genericWrappers
327358
}
328359

329-
return SchemaParserOptions(contextClass, wrappers, allowUnimplementedResolvers, objectMapperProvider, proxyHandlers, preferGraphQLResolver, introspectionEnabled)
360+
return SchemaParserOptions(contextClass, wrappers, allowUnimplementedResolvers, objectMapperProvider,
361+
proxyHandlers, preferGraphQLResolver, introspectionEnabled, coroutineContext)
330362
}
331363
}
332364

src/test/groovy/com/coxautodev/graphql/tools/EndToEndSpec.groovy

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import graphql.execution.batched.BatchedExecutionStrategy
77
import graphql.schema.GraphQLSchema
88
import org.reactivestreams.Publisher
99
import org.reactivestreams.Subscriber
10+
import org.reactivestreams.tck.TestEnvironment
1011
import spock.lang.Shared
1112
import spock.lang.Specification
1213

@@ -560,4 +561,59 @@ class EndToEndSpec extends Specification {
560561
then:
561562
data.dataFetcherResult.name == "item1"
562563
}
564+
565+
def "generated schema supports Kotlin suspend functions"() {
566+
when:
567+
def data = Utils.assertNoGraphQlErrors(gql) {
568+
'''
569+
{
570+
coroutineItems {
571+
id
572+
name
573+
}
574+
}
575+
'''
576+
}
577+
578+
then:
579+
data.coroutineItems == [[id:0, name:"item1"], [id:1, name:"item2"]]
580+
}
581+
582+
def "generated schema supports Kotlin coroutine channels for the subscription query"() {
583+
when:
584+
def newItem = new Item(1, "item", Type.TYPE_1, UUID.randomUUID(), [])
585+
def data = Utils.assertNoGraphQlErrors(gql, [:], new OnItemCreatedContext(newItem)) {
586+
'''
587+
subscription {
588+
onItemCreatedCoroutineChannel {
589+
id
590+
}
591+
}
592+
'''
593+
}
594+
def subscriber = new TestEnvironment().newManualSubscriber(data as Publisher<ExecutionResult>)
595+
596+
then:
597+
subscriber.requestNextElement().data.get("onItemCreatedCoroutineChannel").id == 1
598+
subscriber.expectCompletion()
599+
}
600+
601+
def "generated schema supports Kotlin coroutine channels with suspend function for the subscription query"() {
602+
when:
603+
def newItem = new Item(1, "item", Type.TYPE_1, UUID.randomUUID(), [])
604+
def data = Utils.assertNoGraphQlErrors(gql, [:], new OnItemCreatedContext(newItem)) {
605+
'''
606+
subscription {
607+
onItemCreatedCoroutineChannelAndSuspendFunction {
608+
id
609+
}
610+
}
611+
'''
612+
}
613+
def subscriber = new TestEnvironment().newManualSubscriber(data as Publisher<ExecutionResult>)
614+
615+
then:
616+
subscriber.requestNextElement().data.get("onItemCreatedCoroutineChannelAndSuspendFunction").id == 1
617+
subscriber.expectCompletion()
618+
}
563619
}

src/test/kotlin/com/coxautodev/graphql/tools/EndToEndSpec.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import graphql.language.StringValue
77
import graphql.schema.Coercing
88
import graphql.schema.DataFetchingEnvironment
99
import graphql.schema.GraphQLScalarType
10+
import kotlinx.coroutines.*
11+
import kotlinx.coroutines.channels.Channel
12+
import kotlinx.coroutines.channels.ReceiveChannel
1013
import org.reactivestreams.Publisher
1114
import java.util.Optional
1215
import java.util.UUID
@@ -73,6 +76,8 @@ type Query {
7376
7477
propertyField: String!
7578
dataFetcherResult: Item!
79+
80+
coroutineItems: [Item!]!
7681
}
7782
7883
type ExtendedType {
@@ -104,6 +109,8 @@ type Mutation {
104109
105110
type Subscription {
106111
onItemCreated: Item!
112+
onItemCreatedCoroutineChannel: Item!
113+
onItemCreatedCoroutineChannelAndSuspendFunction: Item!
107114
}
108115
109116
input ItemSearchInput {
@@ -268,12 +275,13 @@ class Query: GraphQLQueryResolver, ListListResolver<String>() {
268275
fun propertyMapWithComplexItems() = propertyMapWithComplexItems
269276
fun propertyMapWithNestedComplexItems() = propertyMapWithNestedComplexItems
270277

271-
272278
private val propertyField = "test"
273279

274280
fun dataFetcherResult(): DataFetcherResult<Item> {
275281
return DataFetcherResult(items.first(), listOf())
276282
}
283+
284+
suspend fun coroutineItems(): List<Item> = CompletableDeferred(items).await()
277285
}
278286

279287
class UnusedRootResolver: GraphQLQueryResolver
@@ -302,6 +310,20 @@ class Subscription : GraphQLSubscriptionResolver {
302310
subscriber.onNext(env.getContext<OnItemCreatedContext>().newItem)
303311
// subscriber.onComplete()
304312
}
313+
314+
fun onItemCreatedCoroutineChannel(env: DataFetchingEnvironment): ReceiveChannel<Item> {
315+
val channel = Channel<Item>(1)
316+
channel.offer(env.getContext<OnItemCreatedContext>().newItem)
317+
return channel
318+
}
319+
320+
suspend fun onItemCreatedCoroutineChannelAndSuspendFunction(env: DataFetchingEnvironment): ReceiveChannel<Item> {
321+
return coroutineScope {
322+
val channel = Channel<Item>(1)
323+
channel.offer(env.getContext<OnItemCreatedContext>().newItem)
324+
channel
325+
}
326+
}
305327
}
306328

307329
class ItemResolver : GraphQLResolver<Item> {

0 commit comments

Comments
 (0)