Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow data and errors to be returned with DataFetcherResult #342

Merged
merged 4 commits into from Sep 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,46 @@
/*
* Copyright 2019 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.sample.query

import graphql.ExceptionWhileDataFetching
import graphql.execution.DataFetcherResult
import graphql.execution.ExecutionPath
import graphql.language.SourceLocation
import org.springframework.stereotype.Component
import java.util.concurrent.CompletableFuture

@Component
class DataAndErrors : Query {

fun returnDataAndErrors(): DataFetcherResult<String> {
val error = ExceptionWhileDataFetching(ExecutionPath.rootPath(), RuntimeException(), SourceLocation(1, 1))
return DataFetcherResult.newResult<String>()
.data("Hello from data fetcher")
.error(error)
.build()
}

fun completableFutureDataAndErrors(): CompletableFuture<DataFetcherResult<String>> {
val error = ExceptionWhileDataFetching(ExecutionPath.rootPath(), RuntimeException(), SourceLocation(1, 1))
val dataFetcherResult = DataFetcherResult.newResult<String>()
.data("Hello from data fetcher")
.error(error)
.build()

return CompletableFuture.completedFuture(dataFetcherResult)
}
}
Expand Up @@ -62,11 +62,7 @@ internal fun KClass<*>.isUnion(): Boolean =

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

internal fun KClass<*>.isList(): Boolean = this.isSubclassOf(List::class)

internal fun KClass<*>.isArray(): Boolean = this.java.isArray

internal fun KClass<*>.isListType(): Boolean = this.isList() || this.isArray()
internal fun KClass<*>.isListType(): Boolean = this.isSubclassOf(List::class) || this.java.isArray

@Throws(CouldNotGetNameOfKClassException::class)
internal fun KClass<*>.getSimpleName(isInputClass: Boolean = false): String {
Expand Down
Expand Up @@ -17,8 +17,10 @@
package com.expediagroup.graphql.generator.extensions

import com.expediagroup.graphql.exceptions.InvalidListTypeException
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.jvm.jvmErasure

private val primitiveArrayTypes = mapOf(
Expand All @@ -33,6 +35,8 @@ private val primitiveArrayTypes = mapOf(

internal fun KType.getKClass() = this.jvmErasure

internal fun KType.isSubclassOf(kClass: KClass<*>) = this.getKClass().isSubclassOf(kClass)

@Throws(InvalidListTypeException::class)
internal fun KType.getTypeOfFirstArgument(): KType =
this.arguments.firstOrNull()?.type ?: throw InvalidListTypeException(this)
Expand Down
Expand Up @@ -22,18 +22,13 @@ import com.expediagroup.graphql.generator.TypeBuilder
import com.expediagroup.graphql.generator.extensions.getDeprecationReason
import com.expediagroup.graphql.generator.extensions.getFunctionName
import com.expediagroup.graphql.generator.extensions.getGraphQLDescription
import com.expediagroup.graphql.generator.extensions.getKClass
import com.expediagroup.graphql.generator.extensions.getTypeOfFirstArgument
import com.expediagroup.graphql.generator.extensions.getValidArguments
import com.expediagroup.graphql.generator.extensions.safeCast
import com.expediagroup.graphql.generator.types.utils.getWrappedReturnType
import graphql.schema.FieldCoordinates
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLOutputType
import org.reactivestreams.Publisher
import java.util.concurrent.CompletableFuture
import kotlin.reflect.KFunction
import kotlin.reflect.KType
import kotlin.reflect.full.isSubclassOf

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

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

val typeFromHooks = config.hooks.willResolveMonad(fn.returnType)
val returnType = getWrappedReturnType(typeFromHooks)
builder.type(graphQLTypeOf(returnType).safeCast<GraphQLOutputType>())
val graphQLType = builder.build()

val graphQLOutputType = graphQLTypeOf(returnType).safeCast<GraphQLOutputType>()
val graphQLType = builder.type(graphQLOutputType).build()
val coordinates = FieldCoordinates.coordinates(parentName, functionName)

if (!abstract) {
val dataFetcherFactory = config.dataFetcherFactoryProvider.functionDataFetcherFactory(target = target, kFunction = fn)
generator.codeRegistry.dataFetcher(coordinates, dataFetcherFactory)
}

return config.hooks.onRewireGraphQLType(graphQLType, coordinates, codeRegistry).safeCast()
}

private fun getWrappedReturnType(returnType: KType): KType =
when {
returnType.getKClass().isSubclassOf(Publisher::class) -> returnType.getTypeOfFirstArgument()
returnType.classifier == CompletableFuture::class -> returnType.getTypeOfFirstArgument()
else -> returnType
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2019 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.types.utils

import com.expediagroup.graphql.generator.extensions.getTypeOfFirstArgument
import com.expediagroup.graphql.generator.extensions.isSubclassOf
import graphql.execution.DataFetcherResult
import org.reactivestreams.Publisher
import java.util.concurrent.CompletableFuture
import kotlin.reflect.KType

/**
* These are the classes that can be returned from data fetchers (ie functions)
* but we only want to expose the wrapped type in the schema.
*
* [Publisher] is used for subscriptions
* [CompletableFuture] is used for asynchronous results
* [DataFetcherResult] is used for returning data and errors in the same response
*
* We can return the following combination of types:
* Valid type T
* Publisher<T>
* DataFetcherResult<T>
* CompletableFuture<T>
* CompletableFuture<DataFetcherResult<T>>
*/
internal fun getWrappedReturnType(returnType: KType): KType {
smyrick marked this conversation as resolved.
Show resolved Hide resolved
return when {
returnType.isSubclassOf(Publisher::class) -> returnType.getTypeOfFirstArgument()
returnType.isSubclassOf(DataFetcherResult::class) -> returnType.getTypeOfFirstArgument()
returnType.isSubclassOf(CompletableFuture::class) -> {
val wrappedType = returnType.getTypeOfFirstArgument()

if (wrappedType.isSubclassOf(DataFetcherResult::class)) {
return wrappedType.getTypeOfFirstArgument()
}

wrappedType
}
else -> returnType
}
}
Expand Up @@ -27,8 +27,12 @@ import com.expediagroup.graphql.exceptions.InvalidIdTypeException
import com.expediagroup.graphql.extensions.deepName
import com.expediagroup.graphql.testSchemaConfig
import com.expediagroup.graphql.toSchema
import graphql.ExceptionWhileDataFetching
import graphql.GraphQL
import graphql.Scalars
import graphql.execution.DataFetcherResult
import graphql.execution.ExecutionPath
import graphql.language.SourceLocation
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLNonNull
import graphql.schema.GraphQLObjectType
Expand Down Expand Up @@ -353,6 +357,23 @@ class SchemaGeneratorTest {
assertEquals(Scalars.GraphQLID, serialField?.wrappedType)
}

@Test
fun `SchemaGenerator supports DataFetcherResult as a return type`() {
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithDataFetcherResult())), config = testSchemaConfig)

val graphQL = GraphQL.newGraphQL(schema).build()
val result = graphQL.execute("{ dataAndErrors }")
val data = result.getData<Map<String, String>>()
val errors = result.errors

assertNotNull(data)
val res: String? = data["dataAndErrors"]
assertEquals(actual = res, expected = "Hello")

assertNotNull(errors)
assertEquals(expected = 1, actual = errors.size)
}

class QueryObject {
@GraphQLDescription("A GraphQL query method")
fun query(@GraphQLDescription("A GraphQL value") value: Int): Geography = Geography(value, GeoType.CITY, listOf())
Expand Down Expand Up @@ -550,4 +571,11 @@ class SchemaGeneratorTest {
@GraphQLID val serial: UUID,
val type: String
)

class QueryWithDataFetcherResult {
fun dataAndErrors(): DataFetcherResult<String> {
val error = ExceptionWhileDataFetching(ExecutionPath.rootPath(), RuntimeException(), SourceLocation(1, 1))
return DataFetcherResult.newResult<String>().data("Hello").error(error).build()
}
}
}
Expand Up @@ -187,22 +187,6 @@ open class KClassExtensionsTest {
assertFalse(MyTestClass::class.isEnum())
}

@Test
fun `test list extension`() {
assertTrue(listOf(1)::class.isList())
assertTrue(arrayListOf(1)::class.isList())
assertFalse(arrayOf(1)::class.isList())
assertFalse(MyTestClass::class.isList())
}

@Test
fun `test array extension`() {
assertTrue(arrayOf(1)::class.isArray())
assertTrue(intArrayOf(1)::class.isArray())
assertFalse(listOf(1)::class.isArray())
assertFalse(MyTestClass::class.isArray())
}

@Test
fun `test listType extension`() {
assertTrue(arrayOf(1)::class.isListType())
Expand Down
Expand Up @@ -27,6 +27,8 @@ import kotlin.reflect.full.findParameterByName
import kotlin.reflect.full.starProjectedType
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue

internal class KTypeExtensionsKtTest {

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

internal interface SimpleInterface

internal class SimpleClass(val id: String) : SimpleInterface

@Test
fun getTypeOfFirstArgument() {
assertEquals(String::class.starProjectedType, MyClass::listFun.findParameterByName("list")?.type?.getTypeOfFirstArgument())
Expand Down Expand Up @@ -72,6 +78,14 @@ internal class KTypeExtensionsKtTest {
assertEquals(MyClass::class, MyClass::class.starProjectedType.getKClass())
}

@Test
fun isSubclassOf() {
assertTrue(MyClass::class.starProjectedType.isSubclassOf(MyClass::class))
assertTrue(SimpleClass::class.starProjectedType.isSubclassOf(SimpleInterface::class))
assertFalse(SimpleInterface::class.starProjectedType.isSubclassOf(SimpleClass::class))
assertFalse(MyClass::class.starProjectedType.isSubclassOf(SimpleInterface::class))
}

@Test
fun getArrayType() {
assertEquals(Int::class.starProjectedType, IntArray::class.starProjectedType.getWrappedType())
Expand Down
Expand Up @@ -21,18 +21,26 @@ import com.expediagroup.graphql.annotations.GraphQLDescription
import com.expediagroup.graphql.annotations.GraphQLDirective
import com.expediagroup.graphql.annotations.GraphQLIgnore
import com.expediagroup.graphql.annotations.GraphQLName
import com.expediagroup.graphql.exceptions.TypeNotSupportedException
import com.expediagroup.graphql.execution.FunctionDataFetcher
import graphql.ExceptionWhileDataFetching
import graphql.Scalars
import graphql.execution.DataFetcherResult
import graphql.execution.ExecutionPath
import graphql.introspection.Introspection
import graphql.language.SourceLocation
import graphql.schema.DataFetchingEnvironment
import graphql.schema.FieldCoordinates
import graphql.schema.GraphQLList
import graphql.schema.GraphQLNonNull
import graphql.schema.GraphQLTypeUtil
import io.reactivex.Flowable
import org.junit.jupiter.api.Test
import org.reactivestreams.Publisher
import java.util.UUID
import java.util.concurrent.CompletableFuture
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue

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

fun dataFetchingEnvironment(environment: DataFetchingEnvironment): String = environment.field.name

fun dataFetcherResult(): DataFetcherResult<String> {
val error = ExceptionWhileDataFetching(ExecutionPath.rootPath(), RuntimeException(), SourceLocation(1, 1))
return DataFetcherResult.newResult<String>().data("Hello").error(error).build()
}

fun listDataFetcherResult(): DataFetcherResult<List<String>> = DataFetcherResult.newResult<List<String>>().data(listOf("Hello")).build()

fun nullalbeListDataFetcherResult(): DataFetcherResult<List<String?>?> = DataFetcherResult.newResult<List<String?>?>().data(listOf("Hello")).build()

fun dataFetcherCompletableFutureResult(): DataFetcherResult<CompletableFuture<String>> {
val completedFuture = CompletableFuture.completedFuture("Hello")
return DataFetcherResult.newResult<CompletableFuture<String>>().data(completedFuture).build()
}

fun completableFutureDataFetcherResult(): CompletableFuture<DataFetcherResult<String>> {
val dataFetcherResult = DataFetcherResult.newResult<String>().data("Hello").build()
return CompletableFuture.completedFuture(dataFetcherResult)
}
}

@Test
Expand Down Expand Up @@ -205,4 +232,54 @@ internal class FunctionBuilderTest : TypeTestHelper() {
assertEquals(expected = 0, actual = result.arguments.size)
assertEquals("String", (result.type as? GraphQLNonNull)?.wrappedType?.name)
}

@Test
fun `DataFetcherResult return type is valid and unwrapped in the schema`() {
val kFunction = Happy::dataFetcherResult
val result = builder.function(fn = kFunction, parentName = "Query", target = null, abstract = false)

assertEquals("String", (result.type as? GraphQLNonNull)?.wrappedType?.name)
}

@Test
fun `DataFetcherResult of a List is valid and unwrapped in the schema`() {
val kFunction = Happy::listDataFetcherResult
val result = builder.function(fn = kFunction, parentName = "Query", target = null, abstract = false)

assertTrue(result.type is GraphQLNonNull)
val listType = GraphQLTypeUtil.unwrapNonNull(result.type)
assertTrue(listType is GraphQLList)
val stringType = GraphQLTypeUtil.unwrapNonNull(GraphQLTypeUtil.unwrapOne(listType))
assertEquals("String", stringType.name)
}

@Test
fun `DataFetcherResult of a nullable List is valid and unwrapped in the schema`() {
val kFunction = Happy::nullalbeListDataFetcherResult
val result = builder.function(fn = kFunction, parentName = "Query", target = null, abstract = false)

val listType = result.type
assertTrue(listType is GraphQLList)
val stringType = listType.wrappedType
assertEquals("String", stringType.name)
}

@Test
fun `DataFetcherResult of a CompletableFuture is invalid`() {
val kFunction = Happy::dataFetcherCompletableFutureResult

assertFailsWith(TypeNotSupportedException::class) {
builder.function(fn = kFunction, parentName = "Query", target = null, abstract = false)
}
}

@Test
fun `CompletableFuture of a DataFetcherResult is valid and unwrapped in the schema`() {
val kFunction = Happy::completableFutureDataFetcherResult
val result = builder.function(fn = kFunction, parentName = "Query", target = null, abstract = false)

assertTrue(result.type is GraphQLNonNull)
val stringType = GraphQLTypeUtil.unwrapNonNull(result.type)
assertEquals("String", stringType.name)
}
}