Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/Reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -20,7 +21,9 @@ data class SchemaConfiguration(
val executor: Executor,
val timeout: Long?,
val introspection: Boolean = true,
val plugins: MutableMap<KClass<*>, Any>
val plugins: MutableMap<KClass<*>, Any>,

val genericTypeResolver: GenericTypeResolver,
) {
@Suppress("UNCHECKED_CAST")
operator fun <T: Any> get(type: KClass<T>) = plugins[type] as T?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<KClass<*>, Any> = mutableMapOf()

Expand All @@ -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,
)
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,15 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor {
}

private suspend fun <T> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

/**
Expand Down Expand Up @@ -252,19 +254,6 @@ class SchemaBuilderTest {
assertThat(result.extract<String>("data/actor/name"), equalTo("Boguś Linda FULL_LENGTH"))
}

private data class LambdaWrapper(val lambda : () -> Int)

@Test
fun `function properties cannot be handled`(){
expect<SchemaException>("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 {
Expand Down Expand Up @@ -306,15 +295,112 @@ class SchemaBuilderTest {
assertThat(schema.inputTypeByKClass(InputTwo::class), notNullValue())
}

private sealed class Maybe<out T> {
abstract fun get(): T
object Undefined : Maybe<Nothing>() {
override fun get() = throw IllegalArgumentException("Requested value is not defined!")
}
class Defined<U>(val value: U) : Maybe<U>() {
override fun get() = value
}
}

@Test
fun `generic types are not supported`(){
expect<SchemaException>("Generic types are not supported by GraphQL, found kotlin.Pair<kotlin.Int, kotlin.String>"){
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<Maybe<*>>().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<Int>, val anotherValue: String = "foo")

val schema = defaultSchema {
configure { genericTypeResolver = typeResolver }

type<SomeWithGenericType>()
query("definedValueProp") { resolver<SomeWithGenericType> { SomeWithGenericType(Maybe.Defined(33)) } }
query("undefinedValueProp") { resolver<SomeWithGenericType> { SomeWithGenericType(Maybe.Undefined) } }

query("definedValue") { resolver<Maybe<String>> { Maybe.Defined("good!") } }
query("undefinedValue") { resolver<Maybe<String>> { 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<String>("data/definedValueProp/value"), equalTo(33))
}
deserialize(schema.executeBlocking("{undefinedValueProp {anotherValue}}")).let {
assertThat(it.extract<String>("data/undefinedValueProp/anotherValue"), equalTo("foo"))
}
deserialize(schema.executeBlocking("{definedValue}")).let {
assertThat(it.extract<String>("data/definedValue"), equalTo("good!"))
}
expect<IllegalArgumentException>("Requested value is not defined!") {
deserialize(schema.executeBlocking("{undefinedValue}"))
}
expect<IllegalArgumentException>("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<Function0<*>>().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<LambdaWrapper>()

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<String>("data/lambda/lambda"), equalTo(1))
}
}

@Test
Expand Down