@@ -24,11 +24,14 @@ import com.fasterxml.jackson.databind.ObjectMapper
24
24
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
25
25
import graphql.schema.DataFetcher
26
26
import graphql.schema.DataFetchingEnvironment
27
+ import kotlinx.coroutines.CoroutineStart
27
28
import kotlinx.coroutines.GlobalScope
28
29
import kotlinx.coroutines.async
29
30
import kotlinx.coroutines.future.asCompletableFuture
30
31
import java.lang.reflect.InvocationTargetException
31
32
import java.util.concurrent.CompletableFuture
33
+ import kotlin.coroutines.CoroutineContext
34
+ import kotlin.coroutines.EmptyCoroutineContext
32
35
import kotlin.reflect.KFunction
33
36
import kotlin.reflect.KParameter
34
37
import kotlin.reflect.full.callSuspend
@@ -42,19 +45,21 @@ import kotlin.reflect.full.valueParameters
42
45
* @param fn The Kotlin function being invoked
43
46
* @param objectMapper Jackson ObjectMapper that will be used to deserialize environment arguments to the expected function arguments
44
47
*/
48
+ @Suppress(" Detekt.SpreadOperator" )
45
49
open class FunctionDataFetcher (
46
50
private val target : Any? ,
47
51
private val fn : KFunction <* >,
48
52
private val objectMapper : ObjectMapper = jacksonObjectMapper()
49
53
) : DataFetcher<Any> {
50
54
55
+ /* *
56
+ * Invoke a suspend function or blocking function, passing in the [target] if not null or default to using the source from the environment.
57
+ */
51
58
override fun get (environment : DataFetchingEnvironment ): Any? {
52
59
val instance = target ? : environment.getSource<Any >()
53
60
54
61
return instance?.let {
55
- val parameterValues = fn.valueParameters
56
- .map { param -> mapParameterToValue(param, environment) }
57
- .toTypedArray()
62
+ val parameterValues = getParameterValues(fn, environment)
58
63
59
64
if (fn.isSuspend) {
60
65
runSuspendingFunction(it, parameterValues)
@@ -64,36 +69,70 @@ open class FunctionDataFetcher(
64
69
}
65
70
}
66
71
67
- private fun mapParameterToValue (param : KParameter , environment : DataFetchingEnvironment ): Any? =
72
+ /* *
73
+ * Iterate over all the function parameters and map them to the proper input values from the environment
74
+ */
75
+ protected open fun getParameterValues (fn : KFunction <* >, environment : DataFetchingEnvironment ): Array <Any ?> = fn.valueParameters
76
+ .map { param -> mapParameterToValue(param, environment) }
77
+ .toTypedArray()
78
+
79
+ /* *
80
+ * Retreives the provided parameter value in the operation input to pass to the function to execute.
81
+ * If the parameter is of a special type then we do not read the input and instead just pass on that value.
82
+ *
83
+ * The special values include:
84
+ * - If the parameter is annotated with [com.expediagroup.graphql.annotations.GraphQLContext],
85
+ * then return the environment context
86
+ *
87
+ * - The entire environment is returned if the parameter is of type [DataFetchingEnvironment]
88
+ */
89
+ protected open fun mapParameterToValue (param : KParameter , environment : DataFetchingEnvironment ): Any? =
68
90
when {
69
91
param.isGraphQLContext() -> environment.getContext()
70
92
param.isDataFetchingEnvironment() -> environment
71
93
else -> convertParameterValue(param, environment)
72
94
}
73
95
74
- private fun convertParameterValue (param : KParameter , environment : DataFetchingEnvironment ): Any? {
96
+ /* *
97
+ * Called to convert the generic input object to the parameter class.
98
+ *
99
+ * This is currently achieved by using a Jackson [ObjectMapper].
100
+ */
101
+ protected open fun convertParameterValue (param : KParameter , environment : DataFetchingEnvironment ): Any? {
75
102
val name = param.getName()
76
103
val klazz = param.javaTypeClass()
77
104
val argument = environment.arguments[name]
78
105
79
106
return objectMapper.convertValue(argument, klazz)
80
107
}
81
108
82
- @Suppress(" Detekt.SpreadOperator" )
83
- private fun runSuspendingFunction (it : Any , parameterValues : Array <Any ?>): CompletableFuture <Any ?> {
84
- return GlobalScope .async {
109
+ /* *
110
+ * Once all parameters values are properly converted, this function will be called to run a suspendable function.
111
+ * If you need to override the exception handling you can override the entire method.
112
+ * You can also call it from [get] with different values to override the default corotuine context or start parameter.
113
+ */
114
+ protected open fun runSuspendingFunction (
115
+ instance : Any ,
116
+ parameterValues : Array <Any ?>,
117
+ coroutineContext : CoroutineContext = EmptyCoroutineContext ,
118
+ coroutineStart : CoroutineStart = CoroutineStart .DEFAULT
119
+ ): CompletableFuture <Any ?> {
120
+ return GlobalScope .async(context = coroutineContext, start = coroutineStart) {
85
121
try {
86
- fn.callSuspend(it , * parameterValues)
122
+ fn.callSuspend(instance , * parameterValues)
87
123
} catch (exception: InvocationTargetException ) {
88
124
throw exception.cause ? : exception
89
125
}
90
126
}.asCompletableFuture()
91
127
}
92
128
93
- @Suppress(" Detekt.SpreadOperator" )
94
- private fun runBlockingFunction (it : Any , parameterValues : Array <Any ?>): Any? {
129
+ /* *
130
+ * Once all parameters values are properly converted, this function will be called to run a simple blocking function.
131
+ * If you need to override the exception handling you can override this method.
132
+ */
133
+ protected open fun runBlockingFunction (instance : Any , parameterValues : Array <Any ?>): Any? {
95
134
try {
96
- return fn.call(it , * parameterValues)
135
+ return fn.call(instance , * parameterValues)
97
136
} catch (exception: InvocationTargetException ) {
98
137
throw exception.cause ? : exception
99
138
}
0 commit comments