All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.expediagroup.graphql.execution.FunctionDataFetcher.kt Maven / Gradle / Ivy

/*
 * Copyright 2020 Expedia, Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.expediagroup.graphql.execution

import com.expediagroup.graphql.generator.extensions.getJavaClass
import com.expediagroup.graphql.generator.extensions.getName
import com.expediagroup.graphql.generator.extensions.getTypeOfFirstArgument
import com.expediagroup.graphql.generator.extensions.isDataFetchingEnvironment
import com.expediagroup.graphql.generator.extensions.isGraphQLContext
import com.expediagroup.graphql.generator.extensions.isList
import com.expediagroup.graphql.generator.extensions.isOptionalInputType
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.future.future
import java.lang.reflect.InvocationTargetException
import java.util.concurrent.CompletableFuture
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.callSuspend
import kotlin.reflect.full.valueParameters

/**
 * Simple DataFetcher that invokes target function on the given object.
 *
 * @param target The target object that performs the data fetching, if not specified then this data fetcher will attempt
 *   to use source object from the environment
 * @param fn The Kotlin function being invoked
 * @param objectMapper Jackson ObjectMapper that will be used to deserialize environment arguments to the expected function arguments
 */
@Suppress("Detekt.SpreadOperator")
open class FunctionDataFetcher(
    private val target: Any?,
    private val fn: KFunction<*>,
    private val objectMapper: ObjectMapper = jacksonObjectMapper()
) : DataFetcher {

    /**
     * Invoke a suspend function or blocking function, passing in the [target] if not null or default to using the source from the environment.
     */
    override fun get(environment: DataFetchingEnvironment): Any? {
        val instance = target ?: environment.getSource()

        return instance?.let {
            val parameterValues = getParameterValues(fn, environment)

            if (fn.isSuspend) {
                runSuspendingFunction(it, parameterValues)
            } else {
                runBlockingFunction(it, parameterValues)
            }
        }
    }

    /**
     * Iterate over all the function parameters and map them to the proper input values from the environment
     */
    protected open fun getParameterValues(fn: KFunction<*>, environment: DataFetchingEnvironment): Array = fn.valueParameters
        .map { param -> mapParameterToValue(param, environment) }
        .toTypedArray()

    /**
     * Retreives the provided parameter value in the operation input to pass to the function to execute.
     * If the parameter is of a special type then we do not read the input and instead just pass on that value.
     *
     * The special values include:
     *   - If the parameter is marked as a [com.expediagroup.graphql.execution.GraphQLContext],
     *     then return the environment context
     *
     *   - The entire environment is returned if the parameter is of type [DataFetchingEnvironment]
     */
    protected open fun mapParameterToValue(param: KParameter, environment: DataFetchingEnvironment): Any? =
        when {
            param.isGraphQLContext() -> environment.getContext()
            param.isDataFetchingEnvironment() -> environment
            else -> convertParameterValue(param, environment)
        }

    /**
     * Called to convert the generic input object to the parameter class.
     *
     * This is currently achieved by using a Jackson ObjectMapper.
     */
    protected open fun convertParameterValue(param: KParameter, environment: DataFetchingEnvironment): Any? {
        val name = param.getName()
        val argument = environment.arguments[name]

        return when {
            param.isList() -> {
                val argumentClass = param.type.getTypeOfFirstArgument().getJavaClass()
                val jacksonCollectionType = objectMapper.typeFactory.constructCollectionType(List::class.java, argumentClass)
                objectMapper.convertValue(argument, jacksonCollectionType)
            }
            param.type.isOptionalInputType() -> {
                when {
                    !environment.containsArgument(name) -> OptionalInput.Undefined
                    argument == null -> OptionalInput.Defined(null)
                    else -> {
                        val argumentClass = param.type.getTypeOfFirstArgument().getJavaClass()
                        val value = objectMapper.convertValue(argument, argumentClass)
                        OptionalInput.Defined(value)
                    }
                }
            }
            else -> {
                val javaClass = param.type.getJavaClass()
                objectMapper.convertValue(argument, javaClass)
            }
        }
    }

    /**
     * Once all parameters values are properly converted, this function will be called to run a suspendable function.
     * If you need to override the exception handling you can override the entire method.
     * You can also call it from [get] with different values to override the default coroutine context or start parameter.
     */
    protected open fun runSuspendingFunction(
        instance: Any,
        parameterValues: Array,
        coroutineContext: CoroutineContext = EmptyCoroutineContext,
        coroutineStart: CoroutineStart = CoroutineStart.DEFAULT
    ): CompletableFuture = GlobalScope.future(context = coroutineContext, start = coroutineStart) {
        try {
            fn.callSuspend(instance, *parameterValues)
        } catch (exception: InvocationTargetException) {
            throw exception.cause ?: exception
        }
    }

    /**
     * Once all parameters values are properly converted, this function will be called to run a simple blocking function.
     * If you need to override the exception handling you can override this method.
     */
    protected open fun runBlockingFunction(instance: Any, parameterValues: Array): Any? {
        try {
            return fn.call(instance, *parameterValues)
        } catch (exception: InvocationTargetException) {
            throw exception.cause ?: exception
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy