Skip to content

Commit 15810a7

Browse files
smyrickShane Myrick
andauthored
Allow for custom subscription return types (#644)
* Allow for custom subscription return types * Update subscription docs * Fix linter Co-authored-by: Shane Myrick <[email protected]>
1 parent 38a4f94 commit 15810a7

File tree

5 files changed

+57
-11
lines changed

5 files changed

+57
-11
lines changed

docs/execution/subscriptions.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@ toSchema(
2828

2929
### Subscription Hooks
3030

31-
Through the hooks a new method was added `didGenerateSubscriptionType` which is called after a new subscription type is
32-
generated but before it is added to the schema. The other hook are still called so you can add logic for the types and
31+
#### `didGenerateSubscriptionType`
32+
This hook is called after a new subscription type is generated but before it is added to the schema. The other generator hooks are still called so you can add logic for the types and
3333
validation of subscriptions the same as queries and mutations.
3434

35+
#### `isValidSubscriptionReturnType`
36+
This hook is called when generating the functions for each subscription. It allows for changing the rules of what classes can be used as the return type. By default, graphql-java supports `org.reactivestreams.Publisher`.
37+
38+
To effectively use this hook, you should also override the `willResolveMonad` hook, and if you are using `graphql-kotlin-spring-server` you should override the `GraphQL` bean to specify a custom subscription execution strategy.
39+
3540
### Server Implementation
3641

3742
The server that runs your GraphQL schema will have to support some method for subscriptions, like WebSockets.

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/InvalidSubscriptionTypeException.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import kotlin.reflect.KFunction
2222

2323
class InvalidSubscriptionTypeException(kClass: KClass<*>, kFunction: KFunction<*>? = null) :
2424
GraphQLKotlinException(
25-
"Schema requires all subscriptions to be public and return a type of Publisher. " +
25+
"Schema requires all subscriptions to be public and return a valid type from the hooks. " +
2626
"${kClass.simpleName} has ${kClass.visibility} visibility modifier. " +
2727
if (kFunction != null) "The function return type is ${kFunction.returnType.getSimpleName()}" else ""
2828
)

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/generateSubscription.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ import com.expediagroup.graphql.exceptions.InvalidSubscriptionTypeException
2121
import com.expediagroup.graphql.generator.SchemaGenerator
2222
import com.expediagroup.graphql.generator.extensions.getValidFunctions
2323
import com.expediagroup.graphql.generator.extensions.isNotPublic
24-
import com.expediagroup.graphql.generator.extensions.isSubclassOf
2524
import graphql.schema.GraphQLObjectType
26-
import org.reactivestreams.Publisher
2725

2826
internal fun generateSubscriptions(generator: SchemaGenerator, subscriptions: List<TopLevelObject>): GraphQLObjectType? {
2927
if (subscriptions.isEmpty()) {
@@ -34,18 +32,20 @@ internal fun generateSubscriptions(generator: SchemaGenerator, subscriptions: Li
3432
subscriptionBuilder.name(generator.config.topLevelNames.subscription)
3533

3634
for (subscription in subscriptions) {
37-
if (subscription.kClass.isNotPublic()) {
38-
throw InvalidSubscriptionTypeException(subscription.kClass)
35+
val kClass = subscription.kClass
36+
37+
if (kClass.isNotPublic()) {
38+
throw InvalidSubscriptionTypeException(kClass)
3939
}
4040

41-
subscription.kClass.getValidFunctions(generator.config.hooks)
41+
kClass.getValidFunctions(generator.config.hooks)
4242
.forEach {
43-
if (it.returnType.isSubclassOf(Publisher::class).not()) {
44-
throw InvalidSubscriptionTypeException(subscription.kClass, it)
43+
if (generator.config.hooks.isValidSubscriptionReturnType(kClass, it).not()) {
44+
throw InvalidSubscriptionTypeException(kClass, it)
4545
}
4646

4747
val function = generateFunction(generator, it, generator.config.topLevelNames.subscription, subscription.obj)
48-
val functionFromHook = generator.config.hooks.didGenerateSubscriptionField(subscription.kClass, it, function)
48+
val functionFromHook = generator.config.hooks.didGenerateSubscriptionField(kClass, it, function)
4949
subscriptionBuilder.field(functionFromHook)
5050
}
5151
}

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooks.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.expediagroup.graphql.exceptions.EmptyMutationTypeException
2323
import com.expediagroup.graphql.exceptions.EmptyObjectTypeException
2424
import com.expediagroup.graphql.exceptions.EmptyQueryTypeException
2525
import com.expediagroup.graphql.exceptions.EmptySubscriptionTypeException
26+
import com.expediagroup.graphql.generator.extensions.isSubclassOf
2627
import graphql.schema.FieldCoordinates
2728
import graphql.schema.GraphQLCodeRegistry
2829
import graphql.schema.GraphQLFieldDefinition
@@ -33,6 +34,7 @@ import graphql.schema.GraphQLSchema
3334
import graphql.schema.GraphQLSchemaElement
3435
import graphql.schema.GraphQLType
3536
import graphql.schema.GraphQLTypeUtil
37+
import org.reactivestreams.Publisher
3638
import kotlin.reflect.KClass
3739
import kotlin.reflect.KFunction
3840
import kotlin.reflect.KProperty
@@ -91,6 +93,15 @@ interface SchemaGeneratorHooks {
9193
@Suppress("Detekt.FunctionOnlyReturningConstant")
9294
fun isValidFunction(kClass: KClass<*>, function: KFunction<*>): Boolean = true
9395

96+
/**
97+
* Called when looking at the subscription functions to determine if it is using a valid return type.
98+
* By default, graphql-java supports org.reactivestreams.Publisher in the subscription execution strategy.
99+
* If you want to provide a custom execution strategy, you may need to override this hook.
100+
*
101+
* NOTE: You will most likely need to also override the [willResolveMonad] hook to allow for your custom type to be generated.
102+
*/
103+
fun isValidSubscriptionReturnType(kClass: KClass<*>, function: KFunction<*>): Boolean = function.returnType.isSubclassOf(Publisher::class)
104+
94105
/**
95106
* Called after `willGenerateGraphQLType` and before `didGenerateGraphQLType`.
96107
* Enables you to change the wiring, e.g. apply directives to alter the target type.

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/GenerateSubscriptionTest.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,20 @@ import com.expediagroup.graphql.TopLevelNames
2020
import com.expediagroup.graphql.TopLevelObject
2121
import com.expediagroup.graphql.exceptions.EmptySubscriptionTypeException
2222
import com.expediagroup.graphql.exceptions.InvalidSubscriptionTypeException
23+
import com.expediagroup.graphql.generator.extensions.getTypeOfFirstArgument
24+
import com.expediagroup.graphql.generator.extensions.isSubclassOf
2325
import com.expediagroup.graphql.hooks.SchemaGeneratorHooks
2426
import graphql.schema.GraphQLFieldDefinition
2527
import io.mockk.every
2628
import io.reactivex.rxjava3.core.Flowable
29+
import kotlinx.coroutines.flow.Flow
30+
import kotlinx.coroutines.flow.flowOf
2731
import org.junit.jupiter.api.Test
2832
import org.junit.jupiter.api.assertThrows
2933
import org.reactivestreams.Publisher
3034
import kotlin.reflect.KClass
3135
import kotlin.reflect.KFunction
36+
import kotlin.reflect.KType
3237
import kotlin.test.assertEquals
3338
import kotlin.test.assertFailsWith
3439
import kotlin.test.assertNotNull
@@ -123,6 +128,26 @@ internal class GenerateSubscriptionTest : TypeTestHelper() {
123128
assertEquals(3, result?.fieldDefinitions?.size)
124129
assertNotNull(result?.fieldDefinitions?.find { it.name == "changedField" })
125130
}
131+
132+
@Test
133+
fun `given custom hooks that allow custom subscription return types, it should generate a valid schema`() {
134+
val subscriptions = listOf(TopLevelObject(MyCustomSubscriptionClass()))
135+
136+
class CustomHooks : SchemaGeneratorHooks {
137+
override fun isValidSubscriptionReturnType(kClass: KClass<*>, function: KFunction<*>) = function.returnType.isSubclassOf(Flow::class)
138+
override fun willResolveMonad(type: KType): KType = when {
139+
type.isSubclassOf(Flow::class) -> type.getTypeOfFirstArgument()
140+
else -> this.willResolveMonad(type)
141+
}
142+
}
143+
144+
every { config.hooks } returns CustomHooks()
145+
146+
val result = generateSubscriptions(generator, subscriptions)
147+
148+
assertEquals(1, result?.fieldDefinitions?.size)
149+
assertNotNull(result?.fieldDefinitions?.find { it.name == "number" })
150+
}
126151
}
127152

128153
class MyPublicTestSubscription {
@@ -138,6 +163,11 @@ class MyInvalidSubscriptionClass {
138163
fun number(): Int = 1
139164
}
140165

166+
class MyCustomSubscriptionClass {
167+
@Suppress("Detekt.FunctionOnlyReturningConstant")
168+
fun number(): Flow<Int> = flowOf(1)
169+
}
170+
141171
private class MyPrivateTestSubscription {
142172
fun counter(): Publisher<Int> = Flowable.just(3)
143173
}

0 commit comments

Comments
 (0)