Skip to content

Commit 84e385a

Browse files
authored
Merge pull request #195 from thevietto/support-generic-types
Allow users to provide custom generic type resolver
2 parents 7266db0 + b023f69 commit 84e385a

File tree

7 files changed

+162
-33
lines changed

7 files changed

+162
-33
lines changed

docs/content/Reference/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ KGraphQL schema allows configuration of following properties:
99
|acceptSingleValueAsArray | Schema accepts single argument values as singleton list | `true`
1010
| 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) |
1111
| executor | | [Executor.Parallel](https://github.com/aPureBase/KGraphQL/blob/master/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/Executor.kt) |
12-
12+
| 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) |
1313

1414
*Example*
1515

kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.apurebase.kgraphql.configuration
22

33
import com.apurebase.kgraphql.schema.execution.Executor
4+
import com.apurebase.kgraphql.schema.execution.GenericTypeResolver
45
import com.fasterxml.jackson.databind.ObjectMapper
56
import kotlinx.coroutines.CoroutineDispatcher
67
import kotlin.reflect.KClass
@@ -20,7 +21,9 @@ data class SchemaConfiguration(
2021
val executor: Executor,
2122
val timeout: Long?,
2223
val introspection: Boolean = true,
23-
val plugins: MutableMap<KClass<*>, Any>
24+
val plugins: MutableMap<KClass<*>, Any>,
25+
26+
val genericTypeResolver: GenericTypeResolver,
2427
) {
2528
@Suppress("UNCHECKED_CAST")
2629
operator fun <T: Any> get(type: KClass<T>) = plugins[type] as T?

kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
66
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
77
import com.apurebase.kgraphql.configuration.SchemaConfiguration
88
import com.apurebase.kgraphql.schema.execution.Executor
9+
import com.apurebase.kgraphql.schema.execution.GenericTypeResolver
910
import kotlinx.coroutines.CoroutineDispatcher
1011
import kotlinx.coroutines.Dispatchers
1112
import kotlin.reflect.KClass
@@ -21,6 +22,7 @@ open class SchemaConfigurationDSL {
2122
var executor: Executor = Executor.Parallel
2223
var timeout: Long? = null
2324
var introspection: Boolean = true
25+
var genericTypeResolver: GenericTypeResolver = GenericTypeResolver.DEFAULT
2426

2527
private val plugins: MutableMap<KClass<*>, Any> = mutableMapOf()
2628

@@ -35,16 +37,17 @@ open class SchemaConfigurationDSL {
3537
internal fun build(): SchemaConfiguration {
3638
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, acceptSingleValueAsArray)
3739
return SchemaConfiguration(
38-
useCachingDocumentParser,
39-
documentParserCacheMaximumSize,
40-
objectMapper,
41-
useDefaultPrettyPrinter,
42-
coroutineDispatcher,
43-
wrapErrors,
44-
executor,
45-
timeout,
46-
introspection,
47-
plugins
40+
useCachingDocumentParser = useCachingDocumentParser,
41+
documentParserCacheMaximumSize = documentParserCacheMaximumSize,
42+
objectMapper = objectMapper,
43+
useDefaultPrettyPrinter = useDefaultPrettyPrinter,
44+
coroutineDispatcher = coroutineDispatcher,
45+
wrapErrors = wrapErrors,
46+
executor = executor,
47+
timeout = timeout,
48+
introspection = introspection,
49+
plugins = plugins,
50+
genericTypeResolver = genericTypeResolver,
4851
)
4952
}
5053
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.apurebase.kgraphql.schema.execution
2+
3+
import com.apurebase.kgraphql.schema.SchemaException
4+
import java.util.*
5+
import kotlin.reflect.KType
6+
7+
/**
8+
* A generic type resolver takes values that are wrapped in classes like {@link java.util.Optional} / {@link java.util.OptionalInt} etc..
9+
* and returns value from them. You can provide your own implementation if you have your own specific
10+
* holder classes.
11+
*/
12+
interface GenericTypeResolver {
13+
14+
fun unbox(obj: Any): Any?
15+
16+
fun resolveMonad(type: KType): KType
17+
18+
companion object {
19+
val DEFAULT = DefaultGenericTypeResolver()
20+
}
21+
}
22+
23+
open class DefaultGenericTypeResolver : GenericTypeResolver {
24+
25+
override fun unbox(obj: Any): Any? = obj
26+
27+
override fun resolveMonad(type: KType): KType =
28+
throw SchemaException("Could not resolve resulting type for monad $type. " +
29+
"Please provide custom GenericTypeResolver to KGraphQL configuration to register your generic types")
30+
}

kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,15 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor {
111111
}
112112

113113
private suspend fun <T> createNode(ctx: ExecutionContext, value: T?, node: Execution.Node, returnType: Type): JsonNode {
114-
return when {
115-
value == null -> createNullNode(node, returnType)
114+
if (value == null) {
115+
return createNullNode(node, returnType)
116+
}
117+
val unboxed = schema.configuration.genericTypeResolver.unbox(value)
118+
if (unboxed !== value) {
119+
return createNode(ctx, unboxed, node, returnType)
120+
}
116121

122+
return when {
117123
//check value, not returnType, because this method can be invoked with element value
118124
value is Collection<*> || value is Array<*> -> {
119125
val values: Collection<*> = when (value) {

kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ class SchemaCompilation(
165165
kType.jvmErasure == Context::class && typeCategory == TypeCategory.INPUT -> contextType
166166
kType.jvmErasure == Execution.Node::class && typeCategory == TypeCategory.INPUT -> executionType
167167
kType.jvmErasure == Context::class && typeCategory == TypeCategory.QUERY -> throw SchemaException("Context type cannot be part of schema")
168-
kType.arguments.isNotEmpty() -> throw SchemaException("Generic types are not supported by GraphQL, found $kType")
168+
kType.arguments.isNotEmpty() -> configuration.genericTypeResolver.resolveMonad(kType)
169+
.let { handlePossiblyWrappedType(it, typeCategory) }
169170
kType.jvmErasure.isSealed -> TypeDef.Union(
170171
name = kType.jvmErasure.simpleName!!,
171172
members = kType.jvmErasure.sealedSubclasses.toSet(),

kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaBuilderTest.kt

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.apurebase.kgraphql.schema
33
import com.apurebase.kgraphql.*
44
import com.apurebase.kgraphql.schema.dsl.SchemaBuilder
55
import com.apurebase.kgraphql.schema.dsl.types.TypeDSL
6+
import com.apurebase.kgraphql.schema.execution.DefaultGenericTypeResolver
67
import com.apurebase.kgraphql.schema.introspection.TypeKind
78
import com.apurebase.kgraphql.schema.scalar.StringScalarCoercion
89
import com.apurebase.kgraphql.schema.structure.Field
@@ -14,6 +15,7 @@ import org.hamcrest.MatcherAssert.assertThat
1415
import org.junit.jupiter.api.Test
1516
import java.util.*
1617
import kotlin.reflect.KType
18+
import kotlin.reflect.full.isSupertypeOf
1719
import kotlin.reflect.typeOf
1820

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

255-
private data class LambdaWrapper(val lambda : () -> Int)
256-
257-
@Test
258-
fun `function properties cannot be handled`(){
259-
expect<SchemaException>("Generic types are not supported by GraphQL, found () -> kotlin.Int"){
260-
KGraphQL.schema {
261-
query("lambda"){
262-
resolver { -> LambdaWrapper { 1 } }
263-
}
264-
}
265-
}
266-
}
267-
268257
@Test
269258
fun `java arrays should be supported`() {
270259
KGraphQL.schema {
@@ -306,15 +295,112 @@ class SchemaBuilderTest {
306295
assertThat(schema.inputTypeByKClass(InputTwo::class), notNullValue())
307296
}
308297

298+
private sealed class Maybe<out T> {
299+
abstract fun get(): T
300+
object Undefined : Maybe<Nothing>() {
301+
override fun get() = throw IllegalArgumentException("Requested value is not defined!")
302+
}
303+
class Defined<U>(val value: U) : Maybe<U>() {
304+
override fun get() = value
305+
}
306+
}
307+
309308
@Test
310-
fun `generic types are not supported`(){
311-
expect<SchemaException>("Generic types are not supported by GraphQL, found kotlin.Pair<kotlin.Int, kotlin.String>"){
312-
defaultSchema {
313-
query("data"){
314-
resolver { int: Int, string: String -> int to string }
309+
fun `client code can declare custom generic type resolver`(){
310+
val typeResolver = object : DefaultGenericTypeResolver() {
311+
override fun unbox(obj: Any) = if (obj is Maybe<*>) obj.get() else super.unbox(obj)
312+
override fun resolveMonad(type: KType): KType {
313+
if (typeOf<Maybe<*>>().isSupertypeOf(type)) {
314+
return type.arguments.first().type
315+
?: throw SchemaException("Could not get the type of the first argument for the type $type")
316+
}
317+
return super.resolveMonad(type)
318+
}
319+
}
320+
321+
data class SomeWithGenericType(val value: Maybe<Int>, val anotherValue: String = "foo")
322+
323+
val schema = defaultSchema {
324+
configure { genericTypeResolver = typeResolver }
325+
326+
type<SomeWithGenericType>()
327+
query("definedValueProp") { resolver<SomeWithGenericType> { SomeWithGenericType(Maybe.Defined(33)) } }
328+
query("undefinedValueProp") { resolver<SomeWithGenericType> { SomeWithGenericType(Maybe.Undefined) } }
329+
330+
query("definedValue") { resolver<Maybe<String>> { Maybe.Defined("good!") } }
331+
query("undefinedValue") { resolver<Maybe<String>> { Maybe.Undefined } }
332+
}
333+
334+
deserialize(schema.executeBlocking("{__schema{queryType{fields{ type { ofType { kind name fields { type {ofType {kind name}}}}}}}}}")).let {
335+
assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/kind"), equalTo("OBJECT"))
336+
assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/name"), equalTo("SomeWithGenericType"))
337+
assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/fields[0]/type/ofType/kind"), equalTo("SCALAR"))
338+
assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/fields[0]/type/ofType/name"), equalTo("String"))
339+
340+
assertThat(it.extract("data/__schema/queryType/fields[1]/type/ofType/kind"), equalTo("OBJECT"))
341+
assertThat(it.extract("data/__schema/queryType/fields[1]/type/ofType/name"), equalTo("SomeWithGenericType"))
342+
assertThat(it.extract("data/__schema/queryType/fields[1]/type/ofType/fields[0]/type/ofType/kind"), equalTo("SCALAR"))
343+
assertThat(it.extract("data/__schema/queryType/fields[1]/type/ofType/fields[0]/type/ofType/name"), equalTo("String"))
344+
345+
assertThat(it.extract("data/__schema/queryType/fields[2]/type/ofType/kind"), equalTo("SCALAR"))
346+
assertThat(it.extract("data/__schema/queryType/fields[2]/type/ofType/name"), equalTo("String"))
347+
348+
assertThat(it.extract("data/__schema/queryType/fields[3]/type/ofType/kind"), equalTo("SCALAR"))
349+
assertThat(it.extract("data/__schema/queryType/fields[3]/type/ofType/name"), equalTo("String"))
350+
}
351+
352+
deserialize(schema.executeBlocking("{definedValueProp {value}}")).let {
353+
assertThat(it.extract<String>("data/definedValueProp/value"), equalTo(33))
354+
}
355+
deserialize(schema.executeBlocking("{undefinedValueProp {anotherValue}}")).let {
356+
assertThat(it.extract<String>("data/undefinedValueProp/anotherValue"), equalTo("foo"))
357+
}
358+
deserialize(schema.executeBlocking("{definedValue}")).let {
359+
assertThat(it.extract<String>("data/definedValue"), equalTo("good!"))
360+
}
361+
expect<IllegalArgumentException>("Requested value is not defined!") {
362+
deserialize(schema.executeBlocking("{undefinedValue}"))
363+
}
364+
expect<IllegalArgumentException>("Requested value is not defined!") {
365+
deserialize(schema.executeBlocking("{undefinedValueProp {value}}"))
366+
}
367+
}
368+
369+
data class LambdaWrapper(val lambda : () -> Int)
370+
371+
@Test
372+
fun `function properties can be handled by providing generic type resolver`() {
373+
val typeResolver = object : DefaultGenericTypeResolver() {
374+
override fun unbox(obj: Any) = if (obj is Function0<*>) obj() else super.unbox(obj)
375+
override fun resolveMonad(type: KType): KType {
376+
if (typeOf<Function0<*>>().isSupertypeOf(type)) {
377+
return type.arguments.first().type
378+
?: throw SchemaException("Could not get the type of the first argument for the type $type")
315379
}
380+
return super.resolveMonad(type)
381+
}
382+
}
383+
384+
val schema = defaultSchema {
385+
configure { genericTypeResolver = typeResolver }
386+
387+
type<LambdaWrapper>()
388+
389+
query("lambda"){
390+
resolver { -> LambdaWrapper { 1 } }
316391
}
317392
}
393+
394+
deserialize(schema.executeBlocking("{__schema{queryType{fields{ type { ofType { kind name fields { type {ofType {kind name}}}}}}}}}")).let {
395+
assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/kind"), equalTo("OBJECT"))
396+
assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/name"), equalTo("LambdaWrapper"))
397+
assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/fields[0]/type/ofType/kind"), equalTo("SCALAR"))
398+
assertThat(it.extract("data/__schema/queryType/fields[0]/type/ofType/fields[0]/type/ofType/name"), equalTo("Int"))
399+
}
400+
401+
deserialize(schema.executeBlocking("{lambda {lambda}}")).let {
402+
assertThat(it.extract<String>("data/lambda/lambda"), equalTo(1))
403+
}
318404
}
319405

320406
@Test

0 commit comments

Comments
 (0)