@@ -24,11 +24,14 @@ import com.fasterxml.jackson.databind.ObjectMapper
2424import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
2525import graphql.schema.DataFetcher
2626import graphql.schema.DataFetchingEnvironment
27+ import kotlinx.coroutines.CoroutineStart
2728import kotlinx.coroutines.GlobalScope
2829import kotlinx.coroutines.async
2930import kotlinx.coroutines.future.asCompletableFuture
3031import java.lang.reflect.InvocationTargetException
3132import java.util.concurrent.CompletableFuture
33+ import kotlin.coroutines.CoroutineContext
34+ import kotlin.coroutines.EmptyCoroutineContext
3235import kotlin.reflect.KFunction
3336import kotlin.reflect.KParameter
3437import kotlin.reflect.full.callSuspend
@@ -42,19 +45,21 @@ import kotlin.reflect.full.valueParameters
4245 * @param fn The Kotlin function being invoked
4346 * @param objectMapper Jackson ObjectMapper that will be used to deserialize environment arguments to the expected function arguments
4447 */
48+ @Suppress(" Detekt.SpreadOperator" )
4549open class FunctionDataFetcher (
4650 private val target : Any? ,
4751 private val fn : KFunction <* >,
4852 private val objectMapper : ObjectMapper = jacksonObjectMapper()
4953) : DataFetcher<Any> {
5054
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+ */
5158 override fun get (environment : DataFetchingEnvironment ): Any? {
5259 val instance = target ? : environment.getSource<Any >()
5360
5461 return instance?.let {
55- val parameterValues = fn.valueParameters
56- .map { param -> mapParameterToValue(param, environment) }
57- .toTypedArray()
62+ val parameterValues = getParameterValues(fn, environment)
5863
5964 if (fn.isSuspend) {
6065 runSuspendingFunction(it, parameterValues)
@@ -64,36 +69,70 @@ open class FunctionDataFetcher(
6469 }
6570 }
6671
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? =
6890 when {
6991 param.isGraphQLContext() -> environment.getContext()
7092 param.isDataFetchingEnvironment() -> environment
7193 else -> convertParameterValue(param, environment)
7294 }
7395
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? {
75102 val name = param.getName()
76103 val klazz = param.javaTypeClass()
77104 val argument = environment.arguments[name]
78105
79106 return objectMapper.convertValue(argument, klazz)
80107 }
81108
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) {
85121 try {
86- fn.callSuspend(it , * parameterValues)
122+ fn.callSuspend(instance , * parameterValues)
87123 } catch (exception: InvocationTargetException ) {
88124 throw exception.cause ? : exception
89125 }
90126 }.asCompletableFuture()
91127 }
92128
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? {
95134 try {
96- return fn.call(it , * parameterValues)
135+ return fn.call(instance , * parameterValues)
97136 } catch (exception: InvocationTargetException ) {
98137 throw exception.cause ? : exception
99138 }
0 commit comments