Skip to content

Commit 834afea

Browse files
committed
Use LightDataFetcher where possible
1 parent f19e036 commit 834afea

File tree

9 files changed

+140
-71
lines changed

9 files changed

+140
-71
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<groupId>com.graphql-java-kickstart</groupId>
66
<artifactId>graphql-java-tools</artifactId>
7-
<version>13.1.2-SNAPSHOT</version>
7+
<version>14.0.0-LOCAL</version>
88
<packaging>jar</packaging>
99

1010
<name>GraphQL Java Tools</name>

src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolver.kt

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,20 @@ internal abstract class FieldResolver(
2929
/**
3030
* Add source resolver depending on whether or not this is a resolver method
3131
*/
32-
protected fun getSourceResolver(): SourceResolver {
32+
protected fun createSourceResolver(): SourceResolver {
3333
return if (this.search.source != null) {
34-
{ this.search.source }
34+
SourceResolver { _, _ -> this.search.source }
3535
} else {
36-
{ environment ->
37-
val source = environment.getSource<Any>()
38-
?: throw ResolverError("Expected source object to not be null!")
36+
SourceResolver { environment, sourceObject ->
37+
val source = if (sourceObject != null) {
38+
// if source object is known, environment is null as an optimization (LightDataFetcher)
39+
sourceObject
40+
} else {
41+
environment
42+
?: throw ResolverError("Expected DataFetchingEnvironment to not be null!")
43+
environment.getSource<Any>()
44+
?: throw ResolverError("Expected source object to not be null!")
45+
}
3946

4047
if (!this.genericType.isAssignableFrom(source.javaClass)) {
4148
throw ResolverError("Expected source object to be an instance of '${this.genericType.getRawClass().name}' but instead got '${source.javaClass.name}'")
@@ -47,4 +54,7 @@ internal abstract class FieldResolver(
4754
}
4855
}
4956

50-
internal typealias SourceResolver = (DataFetchingEnvironment) -> Any
57+
fun interface SourceResolver {
58+
59+
fun resolve(environment: DataFetchingEnvironment?, source: Any?): Any
60+
}

src/main/kotlin/graphql/kickstart/tools/resolver/MapFieldResolver.kt

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import graphql.kickstart.tools.util.JavaType
88
import graphql.language.FieldDefinition
99
import graphql.schema.DataFetcher
1010
import graphql.schema.DataFetchingEnvironment
11+
import graphql.schema.GraphQLFieldDefinition
12+
import graphql.schema.LightDataFetcher
13+
import java.util.function.Supplier
1114

1215
/**
1316
* @author Nick Weedon
@@ -37,7 +40,7 @@ internal class MapFieldResolver(
3740
}
3841

3942
override fun createDataFetcher(): DataFetcher<*> {
40-
return MapFieldResolverDataFetcher(getSourceResolver(), field.name)
43+
return MapFieldResolverDataFetcher(createSourceResolver(), field.name)
4144
}
4245

4346
override fun scanForMatches(): List<TypeClassMatcher.PotentialMatch> {
@@ -49,15 +52,18 @@ internal class MapFieldResolver(
4952

5053
internal class MapFieldResolverDataFetcher(
5154
private val sourceResolver: SourceResolver,
52-
private val key: String
53-
) : DataFetcher<Any> {
55+
private val key: String,
56+
) : LightDataFetcher<Any> {
5457

55-
override fun get(environment: DataFetchingEnvironment): Any? {
56-
val resolvedSourceObject = sourceResolver(environment)
57-
if (resolvedSourceObject is Map<*, *>) {
58-
return resolvedSourceObject[key]
58+
override fun get(fieldDefinition: GraphQLFieldDefinition, sourceObject: Any, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? {
59+
if (sourceObject is Map<*, *>) {
60+
return sourceObject[key]
5961
} else {
6062
throw RuntimeException("MapFieldResolver attempt to fetch a field from an object instance that was not a map")
6163
}
6264
}
65+
66+
override fun get(environment: DataFetchingEnvironment): Any? {
67+
return get(environment.fieldDefinition, sourceResolver.resolve(environment, null), { environment })
68+
}
6369
}

src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt

Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package graphql.kickstart.tools.resolver
22

33
import com.fasterxml.jackson.core.type.TypeReference
44
import graphql.GraphQLContext
5-
import graphql.TrivialDataFetcher
65
import graphql.kickstart.tools.*
76
import graphql.kickstart.tools.SchemaParserOptions.GenericWrapper
87
import graphql.kickstart.tools.util.JavaType
@@ -12,12 +11,15 @@ import graphql.kickstart.tools.util.unwrap
1211
import graphql.language.*
1312
import graphql.schema.DataFetcher
1413
import graphql.schema.DataFetchingEnvironment
14+
import graphql.schema.GraphQLFieldDefinition
1515
import graphql.schema.GraphQLTypeUtil.isScalar
16+
import graphql.schema.LightDataFetcher
1617
import kotlinx.coroutines.future.future
1718
import org.slf4j.LoggerFactory
1819
import java.lang.reflect.InvocationTargetException
1920
import java.lang.reflect.Method
2021
import java.util.*
22+
import java.util.function.Supplier
2123
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
2224
import kotlin.reflect.full.valueParameters
2325
import kotlin.reflect.jvm.javaType
@@ -122,9 +124,9 @@ internal class MethodFieldResolver(
122124
}
123125

124126
return if (args.isEmpty() && isTrivialDataFetcher(this.method)) {
125-
TrivialMethodFieldResolverDataFetcher(getSourceResolver(), this.method, args, options)
127+
LightMethodFieldResolverDataFetcher(createSourceResolver(), this.method, options)
126128
} else {
127-
MethodFieldResolverDataFetcher(getSourceResolver(), this.method, args, options)
129+
MethodFieldResolverDataFetcher(createSourceResolver(), this.method, args, options)
128130
}
129131
}
130132

@@ -189,69 +191,102 @@ internal class MethodFieldResolver(
189191
override fun toString() = "MethodFieldResolver{method=$method}"
190192
}
191193

192-
internal open class MethodFieldResolverDataFetcher(
194+
internal class MethodFieldResolverDataFetcher(
193195
private val sourceResolver: SourceResolver,
194-
method: Method,
196+
private val method: Method,
195197
private val args: List<ArgumentPlaceholder>,
196198
private val options: SchemaParserOptions,
197199
) : DataFetcher<Any> {
198200

199-
private val resolverMethod = method
200-
private val isSuspendFunction = try {
201-
method.kotlinFunction?.isSuspend == true
202-
} catch (e: InternalError) {
203-
false
204-
}
201+
private val isSuspendFunction = method.isSuspendFunction()
205202

206-
private class CompareGenericWrappers {
207-
companion object : Comparator<GenericWrapper> {
208-
override fun compare(w1: GenericWrapper, w2: GenericWrapper): Int = when {
209-
w1.type.isAssignableFrom(w2.type) -> 1
210-
else -> -1
203+
override fun get(environment: DataFetchingEnvironment): Any? {
204+
val source = sourceResolver.resolve(environment, null)
205+
val args = this.args.map { it(environment) }.toTypedArray()
206+
207+
return if (isSuspendFunction) {
208+
environment.coroutineScope().future(options.coroutineContextProvider.provide()) {
209+
invokeSuspend(source, method, args)?.transformWithGenericWrapper(options.genericWrappers, { environment })
211210
}
211+
} else {
212+
invoke(method, source, args)?.transformWithGenericWrapper(options.genericWrappers, { environment })
212213
}
213214
}
214215

215-
override fun get(environment: DataFetchingEnvironment): Any? {
216-
val source = sourceResolver(environment)
217-
val args = this.args.map { it(environment) }.toTypedArray()
216+
/**
217+
* Function that returns the object used to fetch the data. It can be a DataFetcher or an entity.
218+
*/
219+
@Suppress("unused")
220+
fun getWrappedFetchingObject(environment: DataFetchingEnvironment): Any {
221+
return sourceResolver.resolve(environment, null)
222+
}
223+
}
224+
225+
/**
226+
* Similar to [MethodFieldResolverDataFetcher] but for light data fetchers which do not require the environment to be supplied unless suspend functions or
227+
* generic wrappers are used.
228+
*/
229+
internal class LightMethodFieldResolverDataFetcher(
230+
private val sourceResolver: SourceResolver,
231+
private val method: Method,
232+
private val options: SchemaParserOptions,
233+
) : LightDataFetcher<Any?> {
234+
235+
private val isSuspendFunction = method.isSuspendFunction()
236+
237+
override fun get(fieldDefinition: GraphQLFieldDefinition, sourceObject: Any, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? {
238+
val source = sourceResolver.resolve(null, sourceObject)
218239

219240
return if (isSuspendFunction) {
220-
environment.coroutineScope().future(options.coroutineContextProvider.provide()) {
221-
invokeSuspend(source, resolverMethod, args)?.transformWithGenericWrapper(environment)
241+
environmentSupplier.get().coroutineScope().future(options.coroutineContextProvider.provide()) {
242+
invokeSuspend(source, method, emptyArray())?.transformWithGenericWrapper(options.genericWrappers, environmentSupplier)
222243
}
223244
} else {
224-
invoke(resolverMethod, source, args)?.transformWithGenericWrapper(environment)
245+
invoke(method, source, emptyArray())?.transformWithGenericWrapper(options.genericWrappers, environmentSupplier)
225246
}
226247
}
227248

228-
private fun Any.transformWithGenericWrapper(environment: DataFetchingEnvironment): Any? {
229-
return options.genericWrappers
230-
.asSequence()
231-
.filter { it.type.isInstance(this) }
232-
.sortedWith(CompareGenericWrappers)
233-
.firstOrNull()
234-
?.transformer?.invoke(this, environment) ?: this
249+
override fun get(environment: DataFetchingEnvironment): Any? {
250+
return get(environment.fieldDefinition, sourceResolver.resolve(environment, null), { environment })
235251
}
236252

237253
/**
238-
* Function that returns the object used to fetch the data.
239-
* It can be a DataFetcher or an entity.
254+
* Function that returns the object used to fetch the data. It can be a DataFetcher or an entity.
240255
*/
241256
@Suppress("unused")
242-
open fun getWrappedFetchingObject(environment: DataFetchingEnvironment): Any {
243-
return sourceResolver(environment)
257+
fun getWrappedFetchingObject(environment: DataFetchingEnvironment): Any {
258+
return sourceResolver.resolve(environment, null)
244259
}
245260
}
246261

247-
// TODO use graphql.schema.LightDataFetcher
248-
internal class TrivialMethodFieldResolverDataFetcher(
249-
sourceResolver: SourceResolver,
250-
method: Method,
251-
args: List<ArgumentPlaceholder>,
252-
options: SchemaParserOptions,
253-
) : MethodFieldResolverDataFetcher(sourceResolver, method, args, options),
254-
TrivialDataFetcher<Any> // just to mark it for tracing and optimizations
262+
private fun Any.transformWithGenericWrapper(
263+
genericWrappers: List<GenericWrapper>,
264+
environmentSupplier: Supplier<DataFetchingEnvironment>
265+
): Any? {
266+
return genericWrappers
267+
.asSequence()
268+
.filter { it.type.isInstance(this) }
269+
.sortedWith(CompareGenericWrappers)
270+
.firstOrNull()
271+
?.transformer?.invoke(this, environmentSupplier.get()) ?: this
272+
}
273+
274+
private class CompareGenericWrappers {
275+
companion object : Comparator<GenericWrapper> {
276+
override fun compare(w1: GenericWrapper, w2: GenericWrapper): Int = when {
277+
w1.type.isAssignableFrom(w2.type) -> 1
278+
else -> -1
279+
}
280+
}
281+
}
282+
283+
private fun Method.isSuspendFunction(): Boolean {
284+
return try {
285+
this.kotlinFunction?.isSuspend == true
286+
} catch (e: InternalError) {
287+
false
288+
}
289+
}
255290

256291
private suspend inline fun invokeSuspend(target: Any, resolverMethod: Method, args: Array<Any?>): Any? {
257292
return suspendCoroutineUninterceptedOrReturn { continuation ->

src/main/kotlin/graphql/kickstart/tools/resolver/PropertyFieldResolver.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import graphql.kickstart.tools.TypeClassMatcher
66
import graphql.language.FieldDefinition
77
import graphql.schema.DataFetcher
88
import graphql.schema.DataFetchingEnvironment
9+
import graphql.schema.GraphQLFieldDefinition
10+
import graphql.schema.LightDataFetcher
911
import java.lang.reflect.Field
12+
import java.util.function.Supplier
1013

1114
/**
1215
* @author Andrew Potter
@@ -15,11 +18,11 @@ internal class PropertyFieldResolver(
1518
field: FieldDefinition,
1619
search: FieldResolverScanner.Search,
1720
options: SchemaParserOptions,
18-
private val property: Field
21+
private val property: Field,
1922
) : FieldResolver(field, search, options, property.declaringClass) {
2023

2124
override fun createDataFetcher(): DataFetcher<*> {
22-
return PropertyFieldResolverDataFetcher(getSourceResolver(), property)
25+
return PropertyFieldResolverDataFetcher(createSourceResolver(), property)
2326
}
2427

2528
override fun scanForMatches(): List<TypeClassMatcher.PotentialMatch> {
@@ -28,7 +31,8 @@ internal class PropertyFieldResolver(
2831
field.type,
2932
property.genericType,
3033
genericType,
31-
SchemaClassScanner.FieldTypeReference(property.toString()))
34+
SchemaClassScanner.FieldTypeReference(property.toString())
35+
)
3236
)
3337
}
3438

@@ -37,10 +41,14 @@ internal class PropertyFieldResolver(
3741

3842
internal class PropertyFieldResolverDataFetcher(
3943
private val sourceResolver: SourceResolver,
40-
private val field: Field
41-
) : DataFetcher<Any> {
44+
private val field: Field,
45+
) : LightDataFetcher<Any> {
46+
47+
override fun get(fieldDefinition: GraphQLFieldDefinition, sourceObject: Any, environmentSupplier: Supplier<DataFetchingEnvironment>): Any? {
48+
return field.get(sourceResolver.resolve(null, sourceObject))
49+
}
4250

4351
override fun get(environment: DataFetchingEnvironment): Any? {
44-
return field.get(sourceResolver(environment))
52+
return get(environment.fieldDefinition, sourceResolver.resolve(environment, null), { environment })
4553
}
4654
}

src/main/kotlin/graphql/kickstart/tools/util/Utils.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ internal fun getDocumentation(node: AbstractNode<*>, options: SchemaParserOption
6262
}
6363

6464
/**
65-
* Simple heuristic to check is a method is a trivial data fetcher.
65+
* Simple heuristic to check if a method is a trivial data fetcher.
6666
*
6767
* Requirements are:
68-
* prefixed with get
69-
* must have zero parameters
68+
* - prefixed with get
69+
* - must have zero parameters
7070
*/
7171
internal fun isTrivialDataFetcher(method: Method): Boolean {
7272
return (method.parameterCount == 0
@@ -80,4 +80,3 @@ private fun isBooleanGetter(method: Method) = (method.name.startsWith("is")
8080
|| method.returnType == Boolean::class.java)
8181

8282
internal fun String.snakeToCamelCase(): String = split("_").joinToString(separator = "") { it.replaceFirstChar(Char::titlecase) }
83-

src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import graphql.schema.idl.SchemaDirectiveWiringEnvironment
1010
import org.junit.Test
1111

1212
class DirectiveTest {
13+
1314
@Test
1415
fun `should apply @uppercase directive on field`() {
1516
val schema = SchemaParser.newParser()

src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverDataFetcherTest.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import graphql.language.TypeName
1313
import graphql.schema.DataFetcher
1414
import graphql.schema.DataFetchingEnvironment
1515
import graphql.schema.DataFetchingEnvironmentImpl
16+
import graphql.schema.GraphQLFieldDefinition
17+
import graphql.schema.GraphQLObjectType
1618
import kotlinx.coroutines.Dispatchers
1719
import kotlinx.coroutines.ExperimentalCoroutinesApi
1820
import kotlinx.coroutines.Job
@@ -299,6 +301,12 @@ class MethodFieldResolverDataFetcherTest {
299301
private fun createEnvironment(source: Any = Object(), arguments: Map<String, Any> = emptyMap(), context: GraphQLContext? = null): DataFetchingEnvironment {
300302
return DataFetchingEnvironmentImpl.newDataFetchingEnvironment(buildExecutionContext())
301303
.source(source)
304+
.fieldDefinition(
305+
GraphQLFieldDefinition.newFieldDefinition()
306+
.name("ignored")
307+
.type(GraphQLObjectType.newObject().name("ignored").build())
308+
.build()
309+
)
302310
.arguments(arguments)
303311
.graphQLContext(context)
304312
.build()

src/test/kotlin/graphql/kickstart/tools/TestUtils.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ import graphql.GraphQL
77
private val mapper = ObjectMapper()
88

99
fun assertNoGraphQlErrors(gql: GraphQL, args: Map<String, Any> = mapOf(), context: Map<Any, Any> = mapOf(), closure: () -> String): Map<String, Any> {
10-
val result = gql.execute(ExecutionInput.newExecutionInput()
11-
.query(closure.invoke())
12-
.graphQLContext(context)
13-
.root(context)
14-
.variables(args))
10+
val result = gql.execute(
11+
ExecutionInput.newExecutionInput()
12+
.query(closure.invoke())
13+
.graphQLContext(context)
14+
.root(context)
15+
.variables(args)
16+
)
1517

1618
if (result.errors.isNotEmpty()) {
17-
throw AssertionError("GraphQL result contained errors!\n${result.errors.map { mapper.writeValueAsString(it) }.joinToString { "\n" }}")
19+
throw AssertionError("GraphQL result contained errors!\n${result.errors.map { it.message }.joinToString("\n")}")
1820
}
1921

2022
return result.getData() as Map<String, Any>

0 commit comments

Comments
 (0)