From b023f69781825b8ef00a54366cccd5ff552b2c90 Mon Sep 17 00:00:00 2001 From: Maksim Ignatev Date: Tue, 4 Oct 2022 14:35:16 +0400 Subject: [PATCH] Allow users to provide custom generic type resolver --- docs/content/Reference/configuration.md | 2 +- .../configuration/SchemaConfiguration.kt | 5 +- .../schema/dsl/SchemaConfigurationDSL.kt | 23 ++-- .../schema/execution/GenericTypeResolver.kt | 30 +++++ .../execution/ParallelRequestExecutor.kt | 10 +- .../schema/structure/SchemaCompilation.kt | 3 +- .../kgraphql/schema/SchemaBuilderTest.kt | 122 +++++++++++++++--- 7 files changed, 162 insertions(+), 33 deletions(-) create mode 100644 kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/GenericTypeResolver.kt diff --git a/docs/content/Reference/configuration.md b/docs/content/Reference/configuration.md index 6c7744bf..c64d8c4d 100644 --- a/docs/content/Reference/configuration.md +++ b/docs/content/Reference/configuration.md @@ -9,7 +9,7 @@ KGraphQL schema allows configuration of following properties: |acceptSingleValueAsArray | Schema accepts single argument values as singleton list | `true` | coroutineDispatcher | Schema is using CoroutineDispatcher from this property | [CommonPool](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CommonPool.kt) | | executor | | [Executor.Parallel](https://github.com/aPureBase/KGraphQL/blob/master/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/Executor.kt) | - +| genericTypeResolver | Schema is using generic type resolver from this property | [GenericTypeResolver.DEFAULT](https://github.com/aPureBase/KGraphQL/blob/master/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/GenericTypeResolver.kt) | *Example* diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt index 7e2e3f15..8f2a0089 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt @@ -1,6 +1,7 @@ package com.apurebase.kgraphql.configuration import com.apurebase.kgraphql.schema.execution.Executor +import com.apurebase.kgraphql.schema.execution.GenericTypeResolver import com.fasterxml.jackson.databind.ObjectMapper import kotlinx.coroutines.CoroutineDispatcher import kotlin.reflect.KClass @@ -20,7 +21,9 @@ data class SchemaConfiguration( val executor: Executor, val timeout: Long?, val introspection: Boolean = true, - val plugins: MutableMap, Any> + val plugins: MutableMap, Any>, + + val genericTypeResolver: GenericTypeResolver, ) { @Suppress("UNCHECKED_CAST") operator fun get(type: KClass) = plugins[type] as T? diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt index d4087a1a..4e128f0c 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.apurebase.kgraphql.configuration.SchemaConfiguration import com.apurebase.kgraphql.schema.execution.Executor +import com.apurebase.kgraphql.schema.execution.GenericTypeResolver import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlin.reflect.KClass @@ -21,6 +22,7 @@ open class SchemaConfigurationDSL { var executor: Executor = Executor.Parallel var timeout: Long? = null var introspection: Boolean = true + var genericTypeResolver: GenericTypeResolver = GenericTypeResolver.DEFAULT private val plugins: MutableMap, Any> = mutableMapOf() @@ -35,16 +37,17 @@ open class SchemaConfigurationDSL { internal fun build(): SchemaConfiguration { objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, acceptSingleValueAsArray) return SchemaConfiguration( - useCachingDocumentParser, - documentParserCacheMaximumSize, - objectMapper, - useDefaultPrettyPrinter, - coroutineDispatcher, - wrapErrors, - executor, - timeout, - introspection, - plugins + useCachingDocumentParser = useCachingDocumentParser, + documentParserCacheMaximumSize = documentParserCacheMaximumSize, + objectMapper = objectMapper, + useDefaultPrettyPrinter = useDefaultPrettyPrinter, + coroutineDispatcher = coroutineDispatcher, + wrapErrors = wrapErrors, + executor = executor, + timeout = timeout, + introspection = introspection, + plugins = plugins, + genericTypeResolver = genericTypeResolver, ) } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/GenericTypeResolver.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/GenericTypeResolver.kt new file mode 100644 index 00000000..0630ed43 --- /dev/null +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/GenericTypeResolver.kt @@ -0,0 +1,30 @@ +package com.apurebase.kgraphql.schema.execution + +import com.apurebase.kgraphql.schema.SchemaException +import java.util.* +import kotlin.reflect.KType + +/** + * A generic type resolver takes values that are wrapped in classes like {@link java.util.Optional} / {@link java.util.OptionalInt} etc.. + * and returns value from them. You can provide your own implementation if you have your own specific + * holder classes. + */ +interface GenericTypeResolver { + + fun unbox(obj: Any): Any? + + fun resolveMonad(type: KType): KType + + companion object { + val DEFAULT = DefaultGenericTypeResolver() + } +} + +open class DefaultGenericTypeResolver : GenericTypeResolver { + + override fun unbox(obj: Any): Any? = obj + + override fun resolveMonad(type: KType): KType = + throw SchemaException("Could not resolve resulting type for monad $type. " + + "Please provide custom GenericTypeResolver to KGraphQL configuration to register your generic types") +} \ No newline at end of file diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt index d71b42ac..4a801299 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt @@ -111,9 +111,15 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { } private suspend fun createNode(ctx: ExecutionContext, value: T?, node: Execution.Node, returnType: Type): JsonNode { - return when { - value == null -> createNullNode(node, returnType) + if (value == null) { + return createNullNode(node, returnType) + } + val unboxed = schema.configuration.genericTypeResolver.unbox(value) + if (unboxed !== value) { + return createNode(ctx, unboxed, node, returnType) + } + return when { //check value, not returnType, because this method can be invoked with element value value is Collection<*> || value is Array<*> -> { val values: Collection<*> = when (value) { diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt index 3e898aab..30f2c6fd 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt @@ -165,7 +165,8 @@ class SchemaCompilation( kType.jvmErasure == Context::class && typeCategory == TypeCategory.INPUT -> contextType kType.jvmErasure == Execution.Node::class && typeCategory == TypeCategory.INPUT -> executionType kType.jvmErasure == Context::class && typeCategory == TypeCategory.QUERY -> throw SchemaException("Context type cannot be part of schema") - kType.arguments.isNotEmpty() -> throw SchemaException("Generic types are not supported by GraphQL, found $kType") + kType.arguments.isNotEmpty() -> configuration.genericTypeResolver.resolveMonad(kType) + .let { handlePossiblyWrappedType(it, typeCategory) } kType.jvmErasure.isSealed -> TypeDef.Union( name = kType.jvmErasure.simpleName!!, members = kType.jvmErasure.sealedSubclasses.toSet(), diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaBuilderTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaBuilderTest.kt index 2cc4e89a..25fccb79 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaBuilderTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaBuilderTest.kt @@ -3,6 +3,7 @@ package com.apurebase.kgraphql.schema import com.apurebase.kgraphql.* import com.apurebase.kgraphql.schema.dsl.SchemaBuilder import com.apurebase.kgraphql.schema.dsl.types.TypeDSL +import com.apurebase.kgraphql.schema.execution.DefaultGenericTypeResolver import com.apurebase.kgraphql.schema.introspection.TypeKind import com.apurebase.kgraphql.schema.scalar.StringScalarCoercion import com.apurebase.kgraphql.schema.structure.Field @@ -14,6 +15,7 @@ import org.hamcrest.MatcherAssert.assertThat import org.junit.jupiter.api.Test import java.util.* import kotlin.reflect.KType +import kotlin.reflect.full.isSupertypeOf import kotlin.reflect.typeOf /** @@ -252,19 +254,6 @@ class SchemaBuilderTest { assertThat(result.extract("data/actor/name"), equalTo("BoguĊ› Linda FULL_LENGTH")) } - private data class LambdaWrapper(val lambda : () -> Int) - - @Test - fun `function properties cannot be handled`(){ - expect("Generic types are not supported by GraphQL, found () -> kotlin.Int"){ - KGraphQL.schema { - query("lambda"){ - resolver { -> LambdaWrapper { 1 } } - } - } - } - } - @Test fun `java arrays should be supported`() { KGraphQL.schema { @@ -306,15 +295,112 @@ class SchemaBuilderTest { assertThat(schema.inputTypeByKClass(InputTwo::class), notNullValue()) } + private sealed class Maybe { + abstract fun get(): T + object Undefined : Maybe() { + override fun get() = throw IllegalArgumentException("Requested value is not defined!") + } + class Defined(val value: U) : Maybe() { + override fun get() = value + } + } + @Test - fun `generic types are not supported`(){ - expect("Generic types are not supported by GraphQL, found kotlin.Pair"){ - defaultSchema { - query("data"){ - resolver { int: Int, string: String -> int to string } + fun `client code can declare custom generic type resolver`(){ + val typeResolver = object : DefaultGenericTypeResolver() { + override fun unbox(obj: Any) = if (obj is Maybe<*>) obj.get() else super.unbox(obj) + override fun resolveMonad(type: KType): KType { + if (typeOf>().isSupertypeOf(type)) { + return type.arguments.first().type + ?: throw SchemaException("Could not get the type of the first argument for the type $type") + } + return super.resolveMonad(type) + } + } + + data class SomeWithGenericType(val value: Maybe, val anotherValue: String = "foo") + + val schema = defaultSchema { + configure { genericTypeResolver = typeResolver } + + type() + query("definedValueProp") { resolver { SomeWithGenericType(Maybe.Defined(33)) } } + query("undefinedValueProp") { resolver { SomeWithGenericType(Maybe.Undefined) } } + + query("definedValue") { resolver> { Maybe.Defined("good!") } } + query("undefinedValue") { resolver> { Maybe.Undefined } } + } + + deserialize(schema.executeBlocking("{__schema{queryType{fields{ type { ofType { kind name fields { type {ofType {kind name}}}}}}}}}")).let { + assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/kind"), equalTo("OBJECT")) + assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/name"), equalTo("SomeWithGenericType")) + assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/fields[0]/type/ofType/kind"), equalTo("SCALAR")) + assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/fields[0]/type/ofType/name"), equalTo("String")) + + assertThat(it.extract("data/__schema/queryType/fields[1]/type/ofType/kind"), equalTo("OBJECT")) + assertThat(it.extract("data/__schema/queryType/fields[1]/type/ofType/name"), equalTo("SomeWithGenericType")) + assertThat(it.extract("data/__schema/queryType/fields[1]/type/ofType/fields[0]/type/ofType/kind"), equalTo("SCALAR")) + assertThat(it.extract("data/__schema/queryType/fields[1]/type/ofType/fields[0]/type/ofType/name"), equalTo("String")) + + assertThat(it.extract("data/__schema/queryType/fields[2]/type/ofType/kind"), equalTo("SCALAR")) + assertThat(it.extract("data/__schema/queryType/fields[2]/type/ofType/name"), equalTo("String")) + + assertThat(it.extract("data/__schema/queryType/fields[3]/type/ofType/kind"), equalTo("SCALAR")) + assertThat(it.extract("data/__schema/queryType/fields[3]/type/ofType/name"), equalTo("String")) + } + + deserialize(schema.executeBlocking("{definedValueProp {value}}")).let { + assertThat(it.extract("data/definedValueProp/value"), equalTo(33)) + } + deserialize(schema.executeBlocking("{undefinedValueProp {anotherValue}}")).let { + assertThat(it.extract("data/undefinedValueProp/anotherValue"), equalTo("foo")) + } + deserialize(schema.executeBlocking("{definedValue}")).let { + assertThat(it.extract("data/definedValue"), equalTo("good!")) + } + expect("Requested value is not defined!") { + deserialize(schema.executeBlocking("{undefinedValue}")) + } + expect("Requested value is not defined!") { + deserialize(schema.executeBlocking("{undefinedValueProp {value}}")) + } + } + + data class LambdaWrapper(val lambda : () -> Int) + + @Test + fun `function properties can be handled by providing generic type resolver`() { + val typeResolver = object : DefaultGenericTypeResolver() { + override fun unbox(obj: Any) = if (obj is Function0<*>) obj() else super.unbox(obj) + override fun resolveMonad(type: KType): KType { + if (typeOf>().isSupertypeOf(type)) { + return type.arguments.first().type + ?: throw SchemaException("Could not get the type of the first argument for the type $type") } + return super.resolveMonad(type) + } + } + + val schema = defaultSchema { + configure { genericTypeResolver = typeResolver } + + type() + + query("lambda"){ + resolver { -> LambdaWrapper { 1 } } } } + + deserialize(schema.executeBlocking("{__schema{queryType{fields{ type { ofType { kind name fields { type {ofType {kind name}}}}}}}}}")).let { + assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/kind"), equalTo("OBJECT")) + assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/name"), equalTo("LambdaWrapper")) + assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/fields[0]/type/ofType/kind"), equalTo("SCALAR")) + assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/fields[0]/type/ofType/name"), equalTo("Int")) + } + + deserialize(schema.executeBlocking("{lambda {lambda}}")).let { + assertThat(it.extract("data/lambda/lambda"), equalTo(1)) + } } @Test