Skip to content

Commit 2db374d

Browse files
smyrickdariuszkuc
authored andcommitted
Allow data and errors to be returned with DataFetcherResult (ExpediaGroup#342)
* Allow data and errors to be returned with DataFetcherResult Fixes ExpediaGroup#244 Rebase of ExpediaGroup#245 Kotlin functions can now return a DataFetcherResult instead of just their return type which allows you to modify the errors field with any extra data you need * Move output type monad code into the generator * Move unwrapping logic back to function builder * Add unit test for publisher implementation
1 parent b5cba8b commit 2db374d

File tree

10 files changed

+300
-37
lines changed

10 files changed

+300
-37
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2019 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.sample.query
18+
19+
import graphql.ExceptionWhileDataFetching
20+
import graphql.execution.DataFetcherResult
21+
import graphql.execution.ExecutionPath
22+
import graphql.language.SourceLocation
23+
import org.springframework.stereotype.Component
24+
import java.util.concurrent.CompletableFuture
25+
26+
@Component
27+
class DataAndErrors : Query {
28+
29+
fun returnDataAndErrors(): DataFetcherResult<String> {
30+
val error = ExceptionWhileDataFetching(ExecutionPath.rootPath(), RuntimeException(), SourceLocation(1, 1))
31+
return DataFetcherResult.newResult<String>()
32+
.data("Hello from data fetcher")
33+
.error(error)
34+
.build()
35+
}
36+
37+
fun completableFutureDataAndErrors(): CompletableFuture<DataFetcherResult<String>> {
38+
val error = ExceptionWhileDataFetching(ExecutionPath.rootPath(), RuntimeException(), SourceLocation(1, 1))
39+
val dataFetcherResult = DataFetcherResult.newResult<String>()
40+
.data("Hello from data fetcher")
41+
.error(error)
42+
.build()
43+
44+
return CompletableFuture.completedFuture(dataFetcherResult)
45+
}
46+
}

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kClassExtensions.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,7 @@ internal fun KClass<*>.isUnion(): Boolean =
6262

6363
internal fun KClass<*>.isEnum(): Boolean = this.isSubclassOf(Enum::class)
6464

65-
internal fun KClass<*>.isList(): Boolean = this.isSubclassOf(List::class)
66-
67-
internal fun KClass<*>.isArray(): Boolean = this.java.isArray
68-
69-
internal fun KClass<*>.isListType(): Boolean = this.isList() || this.isArray()
65+
internal fun KClass<*>.isListType(): Boolean = this.isSubclassOf(List::class) || this.java.isArray
7066

7167
@Throws(CouldNotGetNameOfKClassException::class)
7268
internal fun KClass<*>.getSimpleName(isInputClass: Boolean = false): String {

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kTypeExtensions.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package com.expediagroup.graphql.generator.extensions
1818

1919
import com.expediagroup.graphql.exceptions.InvalidListTypeException
20+
import kotlin.reflect.KClass
2021
import kotlin.reflect.KType
2122
import kotlin.reflect.full.createType
23+
import kotlin.reflect.full.isSubclassOf
2224
import kotlin.reflect.jvm.jvmErasure
2325

2426
private val primitiveArrayTypes = mapOf(
@@ -33,6 +35,8 @@ private val primitiveArrayTypes = mapOf(
3335

3436
internal fun KType.getKClass() = this.jvmErasure
3537

38+
internal fun KType.isSubclassOf(kClass: KClass<*>) = this.getKClass().isSubclassOf(kClass)
39+
3640
@Throws(InvalidListTypeException::class)
3741
internal fun KType.getTypeOfFirstArgument(): KType =
3842
this.arguments.firstOrNull()?.type ?: throw InvalidListTypeException(this)

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

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,13 @@ import com.expediagroup.graphql.generator.TypeBuilder
2222
import com.expediagroup.graphql.generator.extensions.getDeprecationReason
2323
import com.expediagroup.graphql.generator.extensions.getFunctionName
2424
import com.expediagroup.graphql.generator.extensions.getGraphQLDescription
25-
import com.expediagroup.graphql.generator.extensions.getKClass
26-
import com.expediagroup.graphql.generator.extensions.getTypeOfFirstArgument
2725
import com.expediagroup.graphql.generator.extensions.getValidArguments
2826
import com.expediagroup.graphql.generator.extensions.safeCast
27+
import com.expediagroup.graphql.generator.types.utils.getWrappedReturnType
2928
import graphql.schema.FieldCoordinates
3029
import graphql.schema.GraphQLFieldDefinition
3130
import graphql.schema.GraphQLOutputType
32-
import org.reactivestreams.Publisher
33-
import java.util.concurrent.CompletableFuture
3431
import kotlin.reflect.KFunction
35-
import kotlin.reflect.KType
36-
import kotlin.reflect.full.isSubclassOf
3732

3833
internal class FunctionBuilder(generator: SchemaGenerator) : TypeBuilder(generator) {
3934

@@ -58,22 +53,15 @@ internal class FunctionBuilder(generator: SchemaGenerator) : TypeBuilder(generat
5853

5954
val typeFromHooks = config.hooks.willResolveMonad(fn.returnType)
6055
val returnType = getWrappedReturnType(typeFromHooks)
61-
builder.type(graphQLTypeOf(returnType).safeCast<GraphQLOutputType>())
62-
val graphQLType = builder.build()
63-
56+
val graphQLOutputType = graphQLTypeOf(returnType).safeCast<GraphQLOutputType>()
57+
val graphQLType = builder.type(graphQLOutputType).build()
6458
val coordinates = FieldCoordinates.coordinates(parentName, functionName)
59+
6560
if (!abstract) {
6661
val dataFetcherFactory = config.dataFetcherFactoryProvider.functionDataFetcherFactory(target = target, kFunction = fn)
6762
generator.codeRegistry.dataFetcher(coordinates, dataFetcherFactory)
6863
}
6964

7065
return config.hooks.onRewireGraphQLType(graphQLType, coordinates, codeRegistry).safeCast()
7166
}
72-
73-
private fun getWrappedReturnType(returnType: KType): KType =
74-
when {
75-
returnType.getKClass().isSubclassOf(Publisher::class) -> returnType.getTypeOfFirstArgument()
76-
returnType.classifier == CompletableFuture::class -> returnType.getTypeOfFirstArgument()
77-
else -> returnType
78-
}
7967
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2019 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.generator.types.utils
18+
19+
import com.expediagroup.graphql.generator.extensions.getTypeOfFirstArgument
20+
import com.expediagroup.graphql.generator.extensions.isSubclassOf
21+
import graphql.execution.DataFetcherResult
22+
import org.reactivestreams.Publisher
23+
import java.util.concurrent.CompletableFuture
24+
import kotlin.reflect.KType
25+
26+
/**
27+
* These are the classes that can be returned from data fetchers (ie functions)
28+
* but we only want to expose the wrapped type in the schema.
29+
*
30+
* [Publisher] is used for subscriptions
31+
* [CompletableFuture] is used for asynchronous results
32+
* [DataFetcherResult] is used for returning data and errors in the same response
33+
*
34+
* We can return the following combination of types:
35+
* Valid type T
36+
* Publisher<T>
37+
* DataFetcherResult<T>
38+
* CompletableFuture<T>
39+
* CompletableFuture<DataFetcherResult<T>>
40+
*/
41+
internal fun getWrappedReturnType(returnType: KType): KType {
42+
return when {
43+
returnType.isSubclassOf(Publisher::class) -> returnType.getTypeOfFirstArgument()
44+
returnType.isSubclassOf(DataFetcherResult::class) -> returnType.getTypeOfFirstArgument()
45+
returnType.isSubclassOf(CompletableFuture::class) -> {
46+
val wrappedType = returnType.getTypeOfFirstArgument()
47+
48+
if (wrappedType.isSubclassOf(DataFetcherResult::class)) {
49+
return wrappedType.getTypeOfFirstArgument()
50+
}
51+
52+
wrappedType
53+
}
54+
else -> returnType
55+
}
56+
}

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ import com.expediagroup.graphql.exceptions.InvalidIdTypeException
2727
import com.expediagroup.graphql.extensions.deepName
2828
import com.expediagroup.graphql.testSchemaConfig
2929
import com.expediagroup.graphql.toSchema
30+
import graphql.ExceptionWhileDataFetching
3031
import graphql.GraphQL
3132
import graphql.Scalars
33+
import graphql.execution.DataFetcherResult
34+
import graphql.execution.ExecutionPath
35+
import graphql.language.SourceLocation
3236
import graphql.schema.GraphQLFieldDefinition
3337
import graphql.schema.GraphQLNonNull
3438
import graphql.schema.GraphQLObjectType
@@ -353,6 +357,23 @@ class SchemaGeneratorTest {
353357
assertEquals(Scalars.GraphQLID, serialField?.wrappedType)
354358
}
355359

360+
@Test
361+
fun `SchemaGenerator supports DataFetcherResult as a return type`() {
362+
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithDataFetcherResult())), config = testSchemaConfig)
363+
364+
val graphQL = GraphQL.newGraphQL(schema).build()
365+
val result = graphQL.execute("{ dataAndErrors }")
366+
val data = result.getData<Map<String, String>>()
367+
val errors = result.errors
368+
369+
assertNotNull(data)
370+
val res: String? = data["dataAndErrors"]
371+
assertEquals(actual = res, expected = "Hello")
372+
373+
assertNotNull(errors)
374+
assertEquals(expected = 1, actual = errors.size)
375+
}
376+
356377
class QueryObject {
357378
@GraphQLDescription("A GraphQL query method")
358379
fun query(@GraphQLDescription("A GraphQL value") value: Int): Geography = Geography(value, GeoType.CITY, listOf())
@@ -550,4 +571,11 @@ class SchemaGeneratorTest {
550571
@GraphQLID val serial: UUID,
551572
val type: String
552573
)
574+
575+
class QueryWithDataFetcherResult {
576+
fun dataAndErrors(): DataFetcherResult<String> {
577+
val error = ExceptionWhileDataFetching(ExecutionPath.rootPath(), RuntimeException(), SourceLocation(1, 1))
578+
return DataFetcherResult.newResult<String>().data("Hello").error(error).build()
579+
}
580+
}
553581
}

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/extensions/KClassExtensionsTest.kt

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -187,22 +187,6 @@ open class KClassExtensionsTest {
187187
assertFalse(MyTestClass::class.isEnum())
188188
}
189189

190-
@Test
191-
fun `test list extension`() {
192-
assertTrue(listOf(1)::class.isList())
193-
assertTrue(arrayListOf(1)::class.isList())
194-
assertFalse(arrayOf(1)::class.isList())
195-
assertFalse(MyTestClass::class.isList())
196-
}
197-
198-
@Test
199-
fun `test array extension`() {
200-
assertTrue(arrayOf(1)::class.isArray())
201-
assertTrue(intArrayOf(1)::class.isArray())
202-
assertFalse(listOf(1)::class.isArray())
203-
assertFalse(MyTestClass::class.isArray())
204-
}
205-
206190
@Test
207191
fun `test listType extension`() {
208192
assertTrue(arrayOf(1)::class.isListType())

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/extensions/KTypeExtensionsKtTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import kotlin.reflect.full.findParameterByName
2727
import kotlin.reflect.full.starProjectedType
2828
import kotlin.test.assertEquals
2929
import kotlin.test.assertFailsWith
30+
import kotlin.test.assertFalse
31+
import kotlin.test.assertTrue
3032

3133
internal class KTypeExtensionsKtTest {
3234

@@ -40,6 +42,10 @@ internal class KTypeExtensionsKtTest {
4042
fun stringFun(string: String) = "hello $string"
4143
}
4244

45+
internal interface SimpleInterface
46+
47+
internal class SimpleClass(val id: String) : SimpleInterface
48+
4349
@Test
4450
fun getTypeOfFirstArgument() {
4551
assertEquals(String::class.starProjectedType, MyClass::listFun.findParameterByName("list")?.type?.getTypeOfFirstArgument())
@@ -72,6 +78,14 @@ internal class KTypeExtensionsKtTest {
7278
assertEquals(MyClass::class, MyClass::class.starProjectedType.getKClass())
7379
}
7480

81+
@Test
82+
fun isSubclassOf() {
83+
assertTrue(MyClass::class.starProjectedType.isSubclassOf(MyClass::class))
84+
assertTrue(SimpleClass::class.starProjectedType.isSubclassOf(SimpleInterface::class))
85+
assertFalse(SimpleInterface::class.starProjectedType.isSubclassOf(SimpleClass::class))
86+
assertFalse(MyClass::class.starProjectedType.isSubclassOf(SimpleInterface::class))
87+
}
88+
7589
@Test
7690
fun getArrayType() {
7791
assertEquals(Int::class.starProjectedType, IntArray::class.starProjectedType.getWrappedType())

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,26 @@ import com.expediagroup.graphql.annotations.GraphQLDescription
2121
import com.expediagroup.graphql.annotations.GraphQLDirective
2222
import com.expediagroup.graphql.annotations.GraphQLIgnore
2323
import com.expediagroup.graphql.annotations.GraphQLName
24+
import com.expediagroup.graphql.exceptions.TypeNotSupportedException
2425
import com.expediagroup.graphql.execution.FunctionDataFetcher
26+
import graphql.ExceptionWhileDataFetching
2527
import graphql.Scalars
28+
import graphql.execution.DataFetcherResult
29+
import graphql.execution.ExecutionPath
2630
import graphql.introspection.Introspection
31+
import graphql.language.SourceLocation
2732
import graphql.schema.DataFetchingEnvironment
2833
import graphql.schema.FieldCoordinates
34+
import graphql.schema.GraphQLList
2935
import graphql.schema.GraphQLNonNull
36+
import graphql.schema.GraphQLTypeUtil
3037
import io.reactivex.Flowable
3138
import org.junit.jupiter.api.Test
3239
import org.reactivestreams.Publisher
3340
import java.util.UUID
3441
import java.util.concurrent.CompletableFuture
3542
import kotlin.test.assertEquals
43+
import kotlin.test.assertFailsWith
3644
import kotlin.test.assertTrue
3745

3846
@Suppress("Detekt.UnusedPrivateClass")
@@ -78,6 +86,25 @@ internal class FunctionBuilderTest : TypeTestHelper() {
7886
fun completableFuture(num: Int): CompletableFuture<Int> = CompletableFuture.completedFuture(num)
7987

8088
fun dataFetchingEnvironment(environment: DataFetchingEnvironment): String = environment.field.name
89+
90+
fun dataFetcherResult(): DataFetcherResult<String> {
91+
val error = ExceptionWhileDataFetching(ExecutionPath.rootPath(), RuntimeException(), SourceLocation(1, 1))
92+
return DataFetcherResult.newResult<String>().data("Hello").error(error).build()
93+
}
94+
95+
fun listDataFetcherResult(): DataFetcherResult<List<String>> = DataFetcherResult.newResult<List<String>>().data(listOf("Hello")).build()
96+
97+
fun nullalbeListDataFetcherResult(): DataFetcherResult<List<String?>?> = DataFetcherResult.newResult<List<String?>?>().data(listOf("Hello")).build()
98+
99+
fun dataFetcherCompletableFutureResult(): DataFetcherResult<CompletableFuture<String>> {
100+
val completedFuture = CompletableFuture.completedFuture("Hello")
101+
return DataFetcherResult.newResult<CompletableFuture<String>>().data(completedFuture).build()
102+
}
103+
104+
fun completableFutureDataFetcherResult(): CompletableFuture<DataFetcherResult<String>> {
105+
val dataFetcherResult = DataFetcherResult.newResult<String>().data("Hello").build()
106+
return CompletableFuture.completedFuture(dataFetcherResult)
107+
}
81108
}
82109

83110
@Test
@@ -205,4 +232,54 @@ internal class FunctionBuilderTest : TypeTestHelper() {
205232
assertEquals(expected = 0, actual = result.arguments.size)
206233
assertEquals("String", (result.type as? GraphQLNonNull)?.wrappedType?.name)
207234
}
235+
236+
@Test
237+
fun `DataFetcherResult return type is valid and unwrapped in the schema`() {
238+
val kFunction = Happy::dataFetcherResult
239+
val result = builder.function(fn = kFunction, parentName = "Query", target = null, abstract = false)
240+
241+
assertEquals("String", (result.type as? GraphQLNonNull)?.wrappedType?.name)
242+
}
243+
244+
@Test
245+
fun `DataFetcherResult of a List is valid and unwrapped in the schema`() {
246+
val kFunction = Happy::listDataFetcherResult
247+
val result = builder.function(fn = kFunction, parentName = "Query", target = null, abstract = false)
248+
249+
assertTrue(result.type is GraphQLNonNull)
250+
val listType = GraphQLTypeUtil.unwrapNonNull(result.type)
251+
assertTrue(listType is GraphQLList)
252+
val stringType = GraphQLTypeUtil.unwrapNonNull(GraphQLTypeUtil.unwrapOne(listType))
253+
assertEquals("String", stringType.name)
254+
}
255+
256+
@Test
257+
fun `DataFetcherResult of a nullable List is valid and unwrapped in the schema`() {
258+
val kFunction = Happy::nullalbeListDataFetcherResult
259+
val result = builder.function(fn = kFunction, parentName = "Query", target = null, abstract = false)
260+
261+
val listType = result.type
262+
assertTrue(listType is GraphQLList)
263+
val stringType = listType.wrappedType
264+
assertEquals("String", stringType.name)
265+
}
266+
267+
@Test
268+
fun `DataFetcherResult of a CompletableFuture is invalid`() {
269+
val kFunction = Happy::dataFetcherCompletableFutureResult
270+
271+
assertFailsWith(TypeNotSupportedException::class) {
272+
builder.function(fn = kFunction, parentName = "Query", target = null, abstract = false)
273+
}
274+
}
275+
276+
@Test
277+
fun `CompletableFuture of a DataFetcherResult is valid and unwrapped in the schema`() {
278+
val kFunction = Happy::completableFutureDataFetcherResult
279+
val result = builder.function(fn = kFunction, parentName = "Query", target = null, abstract = false)
280+
281+
assertTrue(result.type is GraphQLNonNull)
282+
val stringType = GraphQLTypeUtil.unwrapNonNull(result.type)
283+
assertEquals("String", stringType.name)
284+
}
208285
}

0 commit comments

Comments
 (0)