diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/DataLoaderPropertyDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/DataLoaderPropertyDSL.kt index a7d8d757..b812f1a5 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/DataLoaderPropertyDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/DataLoaderPropertyDSL.kt @@ -88,4 +88,8 @@ class DataLoaderPropertyDSL( this.inputValues.addAll(inputValues) } + override fun setReturnType(type: KType) { + // NOOP + } + } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/PropertyDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/PropertyDSL.kt index 4b397fca..52144dd5 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/PropertyDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/PropertyDSL.kt @@ -5,6 +5,7 @@ import com.apurebase.kgraphql.schema.model.FunctionWrapper import com.apurebase.kgraphql.schema.model.InputValueDef import com.apurebase.kgraphql.schema.model.PropertyDef import java.lang.IllegalArgumentException +import kotlin.reflect.KType class PropertyDSL(val name : String, block : PropertyDSL.() -> Unit) : LimitedAccessItemDSL(), ResolverDSL.Target { @@ -62,4 +63,8 @@ class PropertyDSL(val name : String, block : PropertyDSL.() -> override fun addInputValues(inputValues: Collection>) { this.inputValues.addAll(inputValues) } + + override fun setReturnType(type: KType) { + // NOOP + } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/ResolverDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/ResolverDSL.kt index 978e00d8..26653b02 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/ResolverDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/ResolverDSL.kt @@ -2,9 +2,10 @@ package com.apurebase.kgraphql.schema.dsl import com.apurebase.kgraphql.schema.dsl.types.InputValuesDSL import com.apurebase.kgraphql.schema.model.InputValueDef +import kotlin.reflect.* -class ResolverDSL(private val target: Target) { +class ResolverDSL(val target: Target) { fun withArgs(block : InputValuesDSL.() -> Unit){ val inputValuesDSL = InputValuesDSL().apply(block) @@ -13,7 +14,14 @@ class ResolverDSL(private val target: Target) { }) } + @OptIn(ExperimentalStdlibApi::class) + inline fun returns(): ResolverDSL { + target.setReturnType(typeOf()) + return this + } + interface Target { fun addInputValues(inputValues: Collection>) + fun setReturnType(type: KType) } } \ No newline at end of file diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/UnionPropertyDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/UnionPropertyDSL.kt index 54d67345..48af2bf7 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/UnionPropertyDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/UnionPropertyDSL.kt @@ -6,6 +6,7 @@ import com.apurebase.kgraphql.schema.model.InputValueDef import com.apurebase.kgraphql.schema.model.PropertyDef import com.apurebase.kgraphql.schema.model.TypeDef import java.lang.IllegalArgumentException +import kotlin.reflect.KType class UnionPropertyDSL(val name : String, block: UnionPropertyDSL.() -> Unit) : LimitedAccessItemDSL(), ResolverDSL.Target { @@ -69,4 +70,8 @@ class UnionPropertyDSL(val name : String, block: UnionPropertyDSL.() override fun addInputValues(inputValues: Collection>) { this.inputValues.addAll(inputValues) } + + override fun setReturnType(type: KType) { + throw IllegalArgumentException("A return value cannot be set on an Union type") + } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/operations/AbstractOperationDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/operations/AbstractOperationDSL.kt index 1e051acd..ef588537 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/operations/AbstractOperationDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/operations/AbstractOperationDSL.kt @@ -6,6 +6,7 @@ import com.apurebase.kgraphql.schema.dsl.ResolverDSL import com.apurebase.kgraphql.schema.model.FunctionWrapper import com.apurebase.kgraphql.schema.model.InputValueDef import kotlin.reflect.KFunction +import kotlin.reflect.KType abstract class AbstractOperationDSL( @@ -17,10 +18,18 @@ abstract class AbstractOperationDSL( internal var functionWrapper: FunctionWrapper<*>? = null + var explicitReturnType: KType? = null + private fun resolver(function: FunctionWrapper<*>): ResolverDSL { - require(function.hasReturnType()) { - "Resolver for '$name' has no return value" + try { + require(function.hasReturnType()) { + "Resolver for '$name' has no return value" + } + } catch (e: Throwable) { + if ("KotlinReflectionInternalError" !in e.toString()) { + throw e + } } functionWrapper = function @@ -58,5 +67,8 @@ abstract class AbstractOperationDSL( this.inputValues.addAll(inputValues) } + override fun setReturnType(type: KType) { + explicitReturnType = type + } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/operations/MutationDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/operations/MutationDSL.kt index dbeb80e0..bd280049 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/operations/MutationDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/operations/MutationDSL.kt @@ -18,7 +18,8 @@ class MutationDSL( isDeprecated = isDeprecated, deprecationReason = deprecationReason, inputValues = inputValues, - accessRule = accessRuleBlock + accessRule = accessRuleBlock, + explicitReturnType = explicitReturnType ) } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/operations/QueryDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/operations/QueryDSL.kt index 4b67b53e..80d2a9bd 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/operations/QueryDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/operations/QueryDSL.kt @@ -18,7 +18,8 @@ class QueryDSL( isDeprecated = isDeprecated, deprecationReason = deprecationReason, inputValues = inputValues, - accessRule = accessRuleBlock + accessRule = accessRuleBlock, + explicitReturnType = explicitReturnType ) } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/BaseOperationDef.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/BaseOperationDef.kt index c57fff41..935a7af1 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/BaseOperationDef.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/BaseOperationDef.kt @@ -1,10 +1,16 @@ package com.apurebase.kgraphql.schema.model import com.apurebase.kgraphql.Context +import kotlin.reflect.KType abstract class BaseOperationDef( name : String, private val operationWrapper: FunctionWrapper, val inputValues : List>, - val accessRule : ((T?, Context) -> Exception?)? -) : Definition(name), OperationDef, FunctionWrapper by operationWrapper \ No newline at end of file + val accessRule : ((T?, Context) -> Exception?)?, + private val explicitReturnType: KType? +) : Definition(name), OperationDef, FunctionWrapper by operationWrapper { + + val returnType: KType get() = explicitReturnType ?: kFunction.returnType + +} \ No newline at end of file diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/MutationDef.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/MutationDef.kt index 1be14d77..f958479a 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/MutationDef.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/MutationDef.kt @@ -1,6 +1,7 @@ package com.apurebase.kgraphql.schema.model import com.apurebase.kgraphql.Context +import kotlin.reflect.KType class MutationDef ( name : String, @@ -9,5 +10,6 @@ class MutationDef ( override val isDeprecated: Boolean, override val deprecationReason: String?, accessRule: ((Nothing?, Context) -> Exception?)? = null, - inputValues : List> = emptyList() -) : BaseOperationDef(name, resolver, inputValues, accessRule), DescribedDef + inputValues : List> = emptyList(), + explicitReturnType: KType? = null +) : BaseOperationDef(name, resolver, inputValues, accessRule, explicitReturnType), DescribedDef diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/PropertyDef.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/PropertyDef.kt index 388d176c..3e829844 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/PropertyDef.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/PropertyDef.kt @@ -18,8 +18,9 @@ interface PropertyDef : Depreciable, DescribedDef { override val isDeprecated: Boolean = false, override val deprecationReason: String? = null, accessRule : ((T?, Context) -> Exception?)? = null, - inputValues : List> = emptyList() - ) : BaseOperationDef(name, resolver, inputValues, accessRule), PropertyDef + inputValues : List> = emptyList(), + explicitReturnType: KType? = null + ) : BaseOperationDef(name, resolver, inputValues, accessRule, explicitReturnType), PropertyDef /** * [T] -> The Parent Type diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/QueryDef.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/QueryDef.kt index e2863614..149e1c92 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/QueryDef.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/QueryDef.kt @@ -1,6 +1,7 @@ package com.apurebase.kgraphql.schema.model import com.apurebase.kgraphql.Context +import kotlin.reflect.KType class QueryDef ( name : String, @@ -9,5 +10,6 @@ class QueryDef ( override val isDeprecated: Boolean = false, override val deprecationReason: String? = null, accessRule: ((Nothing?, Context) -> Exception?)? = null, - inputValues : List> = emptyList() -) : BaseOperationDef(name, resolver, inputValues, accessRule), DescribedDef \ No newline at end of file + inputValues : List> = emptyList(), + explicitReturnType: KType? = null +) : BaseOperationDef(name, resolver, inputValues, accessRule, explicitReturnType), DescribedDef \ No newline at end of file diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/SubscriptionDef.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/SubscriptionDef.kt index ae77ce20..c9b88851 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/SubscriptionDef.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/SubscriptionDef.kt @@ -1,6 +1,7 @@ package com.apurebase.kgraphql.schema.model import com.apurebase.kgraphql.Context +import kotlin.reflect.KType class SubscriptionDef ( name : String, @@ -9,5 +10,6 @@ class SubscriptionDef ( override val isDeprecated: Boolean, override val deprecationReason: String?, accessRule: ((Nothing?, Context) -> Exception?)? = null, - inputValues : List> = emptyList() -) : BaseOperationDef(name, resolver, inputValues, accessRule), DescribedDef + inputValues : List> = emptyList(), + explicitReturnType: KType? = null +) : BaseOperationDef(name, resolver, inputValues, accessRule, explicitReturnType), DescribedDef 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 f99d3ae4..665d6d00 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 @@ -147,7 +147,7 @@ class SchemaCompilation( ) private suspend fun handleOperation(operation : BaseOperationDef<*, *>) : Field { - val returnType = handlePossiblyWrappedType(operation.kFunction.returnType, TypeCategory.QUERY) + val returnType = handlePossiblyWrappedType(operation.returnType, TypeCategory.QUERY) val inputValues = handleInputValues(operation.name, operation, operation.inputValues) return Field.Function(operation, returnType, inputValues) } @@ -159,18 +159,26 @@ class SchemaCompilation( return Field.Union(unionProperty, unionProperty.nullable, type, inputValues) } - private suspend fun handlePossiblyWrappedType(kType : KType, typeCategory: TypeCategory) : Type = when { - kType.isIterable() -> handleCollectionType(kType, typeCategory) - 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.jvmErasure.isSealed -> TypeDef.Union( - name = kType.jvmErasure.simpleName!!, - members = kType.jvmErasure.sealedSubclasses.toSet(), - description = null - ).let { handleUnionType(it) } - else -> handleSimpleType(kType, typeCategory) + private suspend fun handlePossiblyWrappedType(kType : KType, typeCategory: TypeCategory) : Type = try { + when { + kType.isIterable() -> handleCollectionType(kType, typeCategory) + 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.jvmErasure.isSealed -> TypeDef.Union( + name = kType.jvmErasure.simpleName!!, + members = kType.jvmErasure.sealedSubclasses.toSet(), + description = null + ).let { handleUnionType(it) } + else -> handleSimpleType(kType, typeCategory) + } + } catch (e: Throwable) { + if ("KotlinReflectionInternalError" in e.toString()) { + throw SchemaException("If you construct a query/mutation generically, you must specify the return type T explicitly with resolver{ ... }.returns()") + } else { + throw e + } } private suspend fun handleCollectionType(kType: KType, typeCategory: TypeCategory): Type { 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 5fcf64cf..23b8f089 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaBuilderTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaBuilderTest.kt @@ -1,6 +1,7 @@ package com.apurebase.kgraphql.schema import com.apurebase.kgraphql.* +import com.apurebase.kgraphql.schema.dsl.SchemaBuilder import com.apurebase.kgraphql.schema.introspection.TypeKind import com.apurebase.kgraphql.schema.scalar.StringScalarCoercion import com.apurebase.kgraphql.schema.structure.Field @@ -603,4 +604,45 @@ class SchemaBuilderTest { message shouldBeEqualTo "Resolver for 'main' has no return value" } } + + inline fun SchemaBuilder.createGenericQuery(x: T) { + query("data") { + resolver { -> x }.returns() + } + } + + @Test + fun `specifying return type explicitly allows generic query creation`(){ + val schema = defaultSchema { + createGenericQuery(InputOne("generic")) + } + + assertThat(schema.typeByKClass(InputOne::class), notNullValue()) + } + + inline fun SchemaBuilder.createGenericQueryWithoutReturns(x: T) { + query("data") { + resolver { -> x } + } + } + + @Test + fun `not specifying return value explicitly with generic query creation throws exception`(){ + invoking { + defaultSchema { + createGenericQueryWithoutReturns(InputOne("generic")) + } + } shouldThrow SchemaException::class with { + message shouldBeEqualTo "If you construct a query/mutation generically, you must specify the return type T explicitly with resolver{ ... }.returns()" + } + } + + @Test + fun `specifying return type explicitly allows generic query creation that returns List of T`(){ + val schema = defaultSchema { + createGenericQuery(listOf("generic")) + } + val result = deserialize(schema.executeBlocking("{data}")) + assertThat(result.extract("data/data"), equalTo(listOf("generic"))) + } }