Skip to content

Commit 5583bc5

Browse files
authored
[docs] example usage of Reactor Mono (#599)
* [docs] example usage of Reactor Mono With the removal of evaluation predicates from the `FunctionDataFetcher` we no longer have an option to automatically handle Reactor/RxJava monads. In order to enable monad support we need to configure custom `SchemaGeneratorHook` to generate valid schema as well as custom `FunctionDataFetcher` so it can correctly process monads at runtime. Ideally this sort of additional runtime logic should be handled by hooks/instrumentation but with the current implementation relying on `graphql-java` it is the simplest workaround to get it working. * fix failing unit test
1 parent 54bc1b5 commit 5583bc5

File tree

11 files changed

+247
-128
lines changed

11 files changed

+247
-128
lines changed

docs/execution/async-models.md

Lines changed: 130 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,130 @@
1-
---
2-
id: async-models
3-
title: Async Models
4-
---
5-
By default, `graphql-kotlin-schema-generator` will resolve all functions synchronously, i.e. it will block the
6-
underlying thread while executing the target function. While you could configure your GraphQL server with execution
7-
strategies that execute each query in parallel on some thread pools, instead we highly recommend to utilize asynchronous
8-
programming models.
9-
10-
## Coroutines
11-
12-
`graphql-kotlin-schema-generator` has built-in support for Kotlin coroutines. Provided default
13-
[FunctionDataFetcher](https://github.com/ExpediaDotCom/graphql-kotlin/blob/master/graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/execution/FunctionDataFetcher.kt)
14-
will automatically asynchronously execute suspendable functions and convert the result to `CompletableFuture` expected
15-
by `graphql-java`.
16-
17-
Example
18-
19-
```kotlin
20-
data class User(val id: String, val name: String)
21-
22-
class Query {
23-
suspend fun getUser(id: String): User {
24-
// Your coroutine logic to get user data
25-
}
26-
}
27-
```
28-
29-
will produce the following schema
30-
31-
```graphql
32-
33-
schema {
34-
query: Query
35-
}
36-
37-
type Query {
38-
getUser(id: String!): User
39-
}
40-
41-
type User {
42-
id: String!
43-
name: String!
44-
}
45-
```
46-
47-
## CompletableFuture
48-
49-
`graphql-java` relies on Java `CompletableFuture` for asynchronously processing the requests. In order to simplify the
50-
interop with `graphql-java`, `graphql-kotlin-schema-generator` has a built-in hook which will automatically unwrap a
51-
`CompletableFuture` and use the inner class as the return type in the schema.
52-
53-
```kotlin
54-
data class User(val id: String, val name: String)
55-
56-
class Query {
57-
fun getUser(id: String): CompletableFuture<User> {
58-
// Your logic to get data asynchronously
59-
}
60-
}
61-
```
62-
63-
will result in the exactly the same schema as in the coroutine example above.
64-
65-
## RxJava/Reactor
66-
67-
If you use a different monad type, like `Single` from [RxJava](https://github.com/ReactiveX/RxJava) or `Mono` from
68-
[Project Reactor](https://projectreactor.io/), you just have to provide the logic in
69-
`SchemaGeneratorHooks.willResolveMonad` to unwrap it and return the inner class.
70-
71-
```kotlin
72-
class RxJava2Query {
73-
fun asynchronouslyDo(): Observable<Int> = Observable.just(1)
74-
75-
fun asynchronouslyDoSingle(): Single<Int> = Single.just(1)
76-
77-
fun maybe(): Maybe<Int> = Maybe.empty()
78-
}
79-
80-
private class MonadHooks : SchemaGeneratorHooks {
81-
override fun willResolveMonad(type: KType): KType = when (type.classifier) {
82-
Observable::class, Single::class, Maybe::class -> type.arguments.firstOrNull()?.type
83-
else -> type
84-
} ?: type
85-
}
86-
87-
val configWithRxJavaMonads = getConfig(hooks = MonadHooks())
88-
89-
toSchema(queries = listOf(TopLevelObject(RxJava2Query())), config = configWithRxJavaMonads)
90-
```
91-
92-
This will produce
93-
94-
```graphql
95-
type Query {
96-
asynchronouslyDo(): Int
97-
asynchronouslyDoSingle(): Int
98-
maybe: Int
99-
}
100-
```
101-
102-
You can find additional example on how to configure the hooks in our [unit
103-
tests](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/SchemaGeneratorAsyncTests.kt).
1+
---
2+
id: async-models
3+
title: Async Models
4+
---
5+
By default, `graphql-kotlin-schema-generator` will resolve all functions synchronously, i.e. it will block the
6+
underlying thread while executing the target function. While you could configure your GraphQL server with execution
7+
strategies that execute each query in parallel on some thread pools, instead we highly recommend to utilize asynchronous
8+
programming models.
9+
10+
## Coroutines
11+
12+
`graphql-kotlin-schema-generator` has built-in support for Kotlin coroutines. Provided default
13+
[FunctionDataFetcher](https://github.com/ExpediaDotCom/graphql-kotlin/blob/master/graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/execution/FunctionDataFetcher.kt)
14+
will automatically asynchronously execute suspendable functions and convert the result to `CompletableFuture` expected
15+
by `graphql-java`.
16+
17+
Example
18+
19+
```kotlin
20+
data class User(val id: String, val name: String)
21+
22+
class Query {
23+
suspend fun getUser(id: String): User {
24+
// Your coroutine logic to get user data
25+
}
26+
}
27+
```
28+
29+
will produce the following schema
30+
31+
```graphql
32+
33+
schema {
34+
query: Query
35+
}
36+
37+
type Query {
38+
getUser(id: String!): User
39+
}
40+
41+
type User {
42+
id: String!
43+
name: String!
44+
}
45+
```
46+
47+
## CompletableFuture
48+
49+
`graphql-java` relies on Java `CompletableFuture` for asynchronously processing the requests. In order to simplify the
50+
interop with `graphql-java`, `graphql-kotlin-schema-generator` has a built-in hook which will automatically unwrap a
51+
`CompletableFuture` and use the inner class as the return type in the schema.
52+
53+
```kotlin
54+
data class User(val id: String, val name: String)
55+
56+
class Query {
57+
fun getUser(id: String): CompletableFuture<User> {
58+
// Your logic to get data asynchronously
59+
}
60+
}
61+
```
62+
63+
will result in the exactly the same schema as in the coroutine example above.
64+
65+
## RxJava/Reactor
66+
67+
If you want to use a different monad type, like `Single` from [RxJava](https://github.com/ReactiveX/RxJava) or `Mono` from
68+
[Project Reactor](https://projectreactor.io/), you have to:
69+
70+
1. Create custom `SchemaGeneratorHook` that implements `willResolveMonad` to provide the necessary logic
71+
to correctly unwrap the monad and return the inner class to generate valid schema
72+
73+
```kotlin
74+
class MonadHooks : SchemaGeneratorHooks {
75+
override fun willResolveMonad(type: KType): KType = when (type.classifier) {
76+
Mono::class -> type.arguments.firstOrNull()?.type
77+
else -> type
78+
} ?: type
79+
}
80+
```
81+
82+
2. Provide custom data fetcher that will properly process those monad types.
83+
84+
```kotlin
85+
class CustomFunctionDataFetcher(target: Any?, fn: KFunction<*>, objectMapper: ObjectMapper) : FunctionDataFetcher(target, fn, objectMapper) {
86+
override fun get(environment: DataFetchingEnvironment): Any? = when (val result = super.get(environment)) {
87+
is Mono<*> -> result.toFuture()
88+
else -> result
89+
}
90+
}
91+
92+
class CustomDataFetcherFactoryProvider(
93+
private val objectMapper: ObjectMapper
94+
) : SimpleKotlinDataFetcherFactoryProvider(objectMapper) {
95+
96+
override fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>): DataFetcherFactory<Any> = DataFetcherFactory<Any> {
97+
CustomFunctionDataFetcher(
98+
target = target,
99+
fn = kFunction,
100+
objectMapper = objectMapper)
101+
}
102+
}
103+
```
104+
105+
With the above you can then create your schema as follows:
106+
107+
```kotlin
108+
class ReactorQuery {
109+
fun asynchronouslyDo(): Mono<Int> = Mono.just(1)
110+
}
111+
112+
val configWithReactorMonoMonad = SchemaGeneratorConfig(
113+
supportedPackages = listOf("myPackage"),
114+
hooks = MonadHooks(),
115+
dataFetcherFactoryProvider = CustomDataFetcherFactoryProvider())
116+
117+
toSchema(queries = listOf(TopLevelObject(ReactorQuery())), config = configWithReactorMonoMonad)
118+
```
119+
120+
This will produce
121+
122+
```graphql
123+
type Query {
124+
asynchronouslyDo(): Int
125+
}
126+
```
127+
128+
You can find additional example on how to configure the hooks in our [unit
129+
tests](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/SchemaGeneratorAsyncTests.kt)
130+
and [example app](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/AsyncQuery.kt).

examples/federation/base-app/src/main/kotlin/com/expediagroup/graphql/examples/Application.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
1616

1717
package com.expediagroup.graphql.examples
1818

19-
import com.expediagroup.graphql.examples.extension.CustomFederationSchemaGeneratorHooks
19+
import com.expediagroup.graphql.examples.hooks.CustomFederationSchemaGeneratorHooks
2020
import com.expediagroup.graphql.federation.execution.FederatedTypeRegistry
2121
import org.springframework.boot.autoconfigure.SpringBootApplication
2222
import org.springframework.boot.runApplication
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.examples.extension
17+
package com.expediagroup.graphql.examples.hooks
1818

1919
import com.expediagroup.graphql.federation.FederatedSchemaGeneratorHooks
2020
import com.expediagroup.graphql.federation.execution.FederatedTypeRegistry

examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/Application.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,12 +17,12 @@
1717
package com.expediagroup.graphql.examples
1818

1919
import com.expediagroup.graphql.directives.KotlinDirectiveWiringFactory
20-
import com.expediagroup.graphql.examples.datafetchers.CustomDataFetcherFactoryProvider
21-
import com.expediagroup.graphql.examples.datafetchers.SpringDataFetcherFactory
2220
import com.expediagroup.graphql.examples.directives.CustomDirectiveWiringFactory
2321
import com.expediagroup.graphql.examples.exceptions.CustomDataFetcherExceptionHandler
22+
import com.expediagroup.graphql.examples.execution.CustomDataFetcherFactoryProvider
2423
import com.expediagroup.graphql.examples.execution.MySubscriptionHooks
25-
import com.expediagroup.graphql.examples.extension.CustomSchemaGeneratorHooks
24+
import com.expediagroup.graphql.examples.execution.SpringDataFetcherFactory
25+
import com.expediagroup.graphql.examples.hooks.CustomSchemaGeneratorHooks
2626
import com.expediagroup.graphql.spring.execution.ApolloSubscriptionHooks
2727
import com.fasterxml.jackson.databind.ObjectMapper
2828
import graphql.execution.DataFetcherExceptionHandler
Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,22 +14,30 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.examples.datafetchers
17+
package com.expediagroup.graphql.examples.execution
1818

1919
import com.expediagroup.graphql.execution.SimpleKotlinDataFetcherFactoryProvider
2020
import com.fasterxml.jackson.databind.ObjectMapper
2121
import graphql.schema.DataFetcherFactory
2222
import kotlin.reflect.KClass
23+
import kotlin.reflect.KFunction
2324
import kotlin.reflect.KProperty
2425

2526
/**
2627
* Custom DataFetcherFactory provider that returns custom Spring based DataFetcherFactory for resolving lateinit properties.
2728
*/
2829
class CustomDataFetcherFactoryProvider(
2930
private val springDataFetcherFactory: SpringDataFetcherFactory,
30-
objectMapper: ObjectMapper
31+
private val objectMapper: ObjectMapper
3132
) : SimpleKotlinDataFetcherFactoryProvider(objectMapper) {
3233

34+
override fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>): DataFetcherFactory<Any> = DataFetcherFactory<Any> {
35+
CustomFunctionDataFetcher(
36+
target = target,
37+
fn = kFunction,
38+
objectMapper = objectMapper)
39+
}
40+
3341
override fun propertyDataFetcherFactory(kClass: KClass<*>, kProperty: KProperty<*>): DataFetcherFactory<Any> =
3442
if (kProperty.isLateinit) {
3543
springDataFetcherFactory
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2020 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.examples.execution
18+
19+
import com.expediagroup.graphql.execution.FunctionDataFetcher
20+
import com.fasterxml.jackson.databind.ObjectMapper
21+
import graphql.schema.DataFetchingEnvironment
22+
import reactor.core.publisher.Mono
23+
import kotlin.reflect.KFunction
24+
25+
/**
26+
* Custom function data fetcher that adds support for Reactor Mono.
27+
*/
28+
class CustomFunctionDataFetcher(target: Any?, fn: KFunction<*>, objectMapper: ObjectMapper) : FunctionDataFetcher(target, fn, objectMapper) {
29+
30+
override fun get(environment: DataFetchingEnvironment): Any? = when (val result = super.get(environment)) {
31+
is Mono<*> -> result.toFuture()
32+
else -> result
33+
}
34+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.examples.datafetchers
17+
package com.expediagroup.graphql.examples.execution
1818

1919
import com.expediagroup.graphql.extensions.deepName
2020
import graphql.schema.DataFetcher

0 commit comments

Comments
 (0)