Skip to content

Commit 44fdca1

Browse files
committed
Fixes #198 Add support for lists of Scalar field arguments that are already coerced
graphql-java already converts scalar types and lists of scalar types when used as field arguments. However the MethodFieldResolver basically serialized that object and deserialized the value to use it as argument value while it's not really necessary. The serialization/deserialization also failed when the (Java) type of the scalar is not easily serializable/deserializable (for example with private fields)
1 parent bc6dedb commit 44fdca1

File tree

2 files changed

+145
-3
lines changed

2 files changed

+145
-3
lines changed

src/main/kotlin/com/coxautodev/graphql/tools/MethodFieldResolver.kt

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ import com.esotericsoftware.reflectasm.MethodAccess
55
import com.fasterxml.jackson.core.type.TypeReference
66
import graphql.execution.batched.Batched
77
import graphql.language.FieldDefinition
8+
import graphql.language.ListType
89
import graphql.language.NonNullType
10+
import graphql.language.Type
11+
import graphql.language.TypeName
912
import graphql.schema.DataFetcher
1013
import graphql.schema.DataFetchingEnvironment
14+
import graphql.schema.GraphQLTypeUtil.isScalar
1115
import kotlinx.coroutines.GlobalScope
1216
import kotlinx.coroutines.future.future
1317
import java.lang.reflect.Method
14-
import java.util.*
18+
import java.util.Comparator
19+
import java.util.Optional
1520
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
1621
import kotlin.reflect.full.valueParameters
1722
import kotlin.reflect.jvm.javaType
@@ -89,6 +94,12 @@ internal class MethodFieldResolver(field: FieldDefinition, search: FieldResolver
8994
}
9095
}
9196

97+
if (value != null
98+
&& genericParameterType.unwrap().isAssignableFrom(value.javaClass)
99+
&& isScalarType(environment, definition.type, genericParameterType)) {
100+
return@add value
101+
}
102+
92103
return@add mapper.convertValue(value, typeReference)
93104
}
94105
}
@@ -109,9 +120,24 @@ internal class MethodFieldResolver(field: FieldDefinition, search: FieldResolver
109120
}
110121
}
111122

123+
private fun isScalarType(environment: DataFetchingEnvironment, type: Type<*>, genericParameterType: JavaType): Boolean =
124+
when (type) {
125+
is ListType ->
126+
List::class.java.isAssignableFrom(this.genericType.getRawClass(genericParameterType))
127+
&& isScalarType(environment, type.type, this.genericType.unwrapGenericType(genericParameterType))
128+
is TypeName ->
129+
environment.graphQLSchema?.getType(type.name)?.let { isScalar(it) } ?: false
130+
else ->
131+
false
132+
}
133+
112134
override fun scanForMatches(): List<TypeClassMatcher.PotentialMatch> {
113135
val batched = isBatched(method, search)
114-
val unwrappedGenericType = genericType.unwrapGenericType(try { method.kotlinFunction?.returnType?.javaType ?: method.genericReturnType } catch (e: InternalError) { method.genericReturnType })
136+
val unwrappedGenericType = genericType.unwrapGenericType(try {
137+
method.kotlinFunction?.returnType?.javaType ?: method.genericReturnType
138+
} catch (e: InternalError) {
139+
method.genericReturnType
140+
})
115141
val returnValueMatch = TypeClassMatcher.PotentialMatch.returnValue(field.type, unwrappedGenericType, genericType, SchemaClassScanner.ReturnValueReference(method), batched)
116142

117143
return field.inputValueDefinitions.mapIndexed { i, inputDefinition ->
@@ -148,7 +174,11 @@ open class MethodFieldResolverDataFetcher(private val sourceResolver: SourceReso
148174
// Convert to reflactasm reflection
149175
private val methodAccess = MethodAccess.get(method.declaringClass)!!
150176
private val methodIndex = methodAccess.getIndex(method.name, *method.parameterTypes)
151-
private val isSuspendFunction = try {method.kotlinFunction?.isSuspend == true } catch (e: InternalError) { false }
177+
private val isSuspendFunction = try {
178+
method.kotlinFunction?.isSuspend == true
179+
} catch (e: InternalError) {
180+
false
181+
}
152182

153183
private class CompareGenericWrappers {
154184
companion object : Comparator<GenericWrapper> {
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.coxautodev.graphql.tools
2+
3+
import graphql.ExecutionInput
4+
import graphql.GraphQL
5+
import graphql.language.StringValue
6+
import graphql.schema.Coercing
7+
import graphql.schema.GraphQLScalarType
8+
import org.junit.Assert
9+
import org.junit.Test
10+
11+
class MethodFieldResolverTest {
12+
13+
@Test
14+
fun `should handle scalar types as method input argument`() {
15+
val schema = SchemaParser.newParser()
16+
.schemaString("""
17+
scalar CustomScalar
18+
type Query {
19+
test(input: CustomScalar): Int
20+
}
21+
""".trimIndent()
22+
)
23+
.scalars(customScalarType)
24+
.resolvers(object : GraphQLQueryResolver {
25+
fun test(scalar: CustomScalar) = scalar.value.length
26+
})
27+
.build()
28+
.makeExecutableSchema()
29+
30+
val gql = GraphQL.newGraphQL(schema).build()
31+
32+
val result = gql
33+
.execute(ExecutionInput.newExecutionInput()
34+
.query("""
35+
query Test(${"$"}input: CustomScalar) {
36+
test(input: ${"$"}input)
37+
}
38+
""".trimIndent())
39+
.variables(mapOf("input" to "FooBar"))
40+
.context(Object())
41+
.root(Object()))
42+
43+
Assert.assertEquals(6, result.getData<Map<String, Any>>()["test"])
44+
}
45+
46+
@Test
47+
fun `should handle lists of scalar types`() {
48+
val schema = SchemaParser.newParser()
49+
.schemaString("""
50+
scalar CustomScalar
51+
type Query {
52+
test(input: [CustomScalar]): Int
53+
}
54+
""".trimIndent()
55+
)
56+
.scalars(customScalarType)
57+
.resolvers(object : GraphQLQueryResolver {
58+
fun test(scalars: List<CustomScalar>) = scalars.map { it.value.length }.sum()
59+
})
60+
.build()
61+
.makeExecutableSchema()
62+
63+
val gql = GraphQL.newGraphQL(schema).build()
64+
65+
val result = gql
66+
.execute(ExecutionInput.newExecutionInput()
67+
.query("""
68+
query Test(${"$"}input: [CustomScalar]) {
69+
test(input: ${"$"}input)
70+
}
71+
""".trimIndent())
72+
.variables(mapOf("input" to listOf("Foo", "Bar")))
73+
.context(Object())
74+
.root(Object()))
75+
76+
Assert.assertEquals(6, result.getData<Map<String, Any>>()["test"])
77+
}
78+
79+
/**
80+
* Custom Scalar Class type that doesn't work with Jackson serialization/deserialization
81+
*/
82+
class CustomScalar private constructor(private val internalValue: String) {
83+
val value get() = internalValue
84+
85+
companion object {
86+
fun of(input: Any?) = when (input) {
87+
is String -> CustomScalar(input)
88+
else -> null
89+
}
90+
}
91+
}
92+
93+
private val customScalarType: GraphQLScalarType = GraphQLScalarType.newScalar()
94+
.name("CustomScalar")
95+
.description("customScalar")
96+
.coercing(object : Coercing<CustomScalar, String> {
97+
98+
override fun parseValue(input: Any?) = CustomScalar.of(input)
99+
100+
override fun parseLiteral(input: Any?) = when (input) {
101+
is StringValue -> CustomScalar.of(input.value)
102+
else -> null
103+
}
104+
105+
override fun serialize(dataFetcherResult: Any?) = when (dataFetcherResult) {
106+
is CustomScalar -> dataFetcherResult.value
107+
else -> null
108+
}
109+
})
110+
.build()
111+
112+
}

0 commit comments

Comments
 (0)