-
Notifications
You must be signed in to change notification settings - Fork 360
add custom java mapping support to FunctionDataFetcher #1520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
01b1da1
to
c53f8a0
Compare
Since you would still have to do custom class JacksonFunctionDataFetcher(
private val target: Any?,
private val fn: KFunction<*>,
private val objectMapper: ObjectMapper = jacksonObjectMapper()
) : FunctionDataFetcher(target, fn) {
override fun mapParameterToValue(param: KParameter, environment: DataFetchingEnvironment): Pair<KParameter, Any?>? =
when {
param.isGraphQLContext() -> param to environment.getContext()
param.isDataFetchingEnvironment() -> param to environment
else -> {
val name = param.getName()
if (environment.containsArgument(name) || param.type.isOptionalInputType()) {
val value: Any? = environment.arguments[name]
param to convertArgumentToObject(param, environment, name, value)
} else {
null
}
}
}
/**
* Convert the generic arument value from JSON input to the paramter class.
* This is currently achieved by using a Jackson ObjectMapper.
*/
private fun convertArgumentToObject(
param: KParameter,
environment: DataFetchingEnvironment,
argumentName: String,
argumentValue: Any?
): Any? = when {
param.type.isOptionalInputType() -> {
when {
!environment.containsArgument(argumentName) -> OptionalInput.Undefined
argumentValue == null -> OptionalInput.Defined(null)
else -> {
val paramType = param.type.getTypeOfFirstArgument()
val value = convertValue(paramType, argumentValue)
OptionalInput.Defined(value)
}
}
}
else -> convertValue(param.type, argumentValue)
}
private fun convertValue(
paramType: KType,
argumentValue: Any?
): Any? = when {
paramType.isList() -> {
val argumentClass = paramType.getTypeOfFirstArgument().getJavaClass()
val jacksonCollectionType = objectMapper.typeFactory.constructCollectionType(List::class.java, argumentClass)
objectMapper.convertValue(argumentValue, jacksonCollectionType)
}
paramType.isArray() -> {
val argumentClass = paramType.getWrappedType().getJavaClass()
val jacksonCollectionType = objectMapper.typeFactory.constructArrayType(argumentClass)
objectMapper.convertValue(argumentValue, jacksonCollectionType)
}
else -> {
val javaClass = paramType.getJavaClass()
objectMapper.convertValue(argumentValue, javaClass)
}
}
} NOTE: code above calls internal |
@dariuszkuc yes. that's possible as well. My intension is not to separate the code base and if somebody needs to deserialise an old java pojo it needs only to overwrite one method instead of copy the old 5.x code.
it is easier to understand. with your solution some of our project use the |
I would encourage doing what @dariuszkuc mentioned, you can extend the It is actually then pattern that is used with the |
thank you. Why do you drop the support of java classes? sure it works with a copied JacksonFunctionDataFetcher import com.expediagroup.graphql.generator.annotations.GraphQLName
import com.expediagroup.graphql.generator.exceptions.CouldNotGetNameOfKParameterException
import com.expediagroup.graphql.generator.exceptions.InvalidWrappedTypeException
import com.expediagroup.graphql.generator.execution.FunctionDataFetcher
import com.expediagroup.graphql.generator.execution.GraphQLContext
import com.expediagroup.graphql.generator.execution.OptionalInput
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import graphql.schema.DataFetchingEnvironment
import kotlin.reflect.KAnnotatedElement
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.jvm.jvmErasure
class JacksonFunctionDataFetcher(
private val target: Any?,
private val fn: KFunction<*>,
private val objectMapper: ObjectMapper = jacksonObjectMapper()
) : FunctionDataFetcher(target, fn) {
override fun mapParameterToValue(param: KParameter, environment: DataFetchingEnvironment): Pair<KParameter, Any?>? =
when {
param.isGraphQLContext() -> param to environment.getContext()
param.isDataFetchingEnvironment() -> param to environment
else -> {
val name = param.getName()
if (environment.containsArgument(name) || param.type.isOptionalInputType()) {
val value: Any? = environment.arguments[name]
param to convertArgumentToObject(param, environment, name, value)
}
else {
null
}
}
}
private fun convertArgumentToObject(
param: KParameter,
environment: DataFetchingEnvironment,
argumentName: String,
argumentValue: Any?
): Any? = when {
param.type.isOptionalInputType() -> {
when {
!environment.containsArgument(argumentName) -> OptionalInput.Undefined
argumentValue == null -> OptionalInput.Defined(null)
else -> {
val paramType = param.type.getTypeOfFirstArgument()
val value = convertValue(paramType, argumentValue)
OptionalInput.Defined(value)
}
}
}
else -> convertValue(param.type, argumentValue)
}
private fun convertValue(
paramType: KType,
argumentValue: Any?
): Any? = when {
paramType.isList() -> {
val argumentClass = paramType.getTypeOfFirstArgument().getJavaClass()
val jacksonCollectionType = objectMapper.typeFactory.constructCollectionType(List::class.java, argumentClass)
objectMapper.convertValue(argumentValue, jacksonCollectionType)
}
paramType.isArray() -> {
val argumentClass = paramType.getWrappedType().getJavaClass()
val jacksonCollectionType = objectMapper.typeFactory.constructArrayType(argumentClass)
objectMapper.convertValue(argumentValue, jacksonCollectionType)
}
else -> {
val javaClass = paramType.getJavaClass()
objectMapper.convertValue(argumentValue, javaClass)
}
}
}
internal fun KParameter.isGraphQLContext() = this.type.getKClass().isSubclassOf(GraphQLContext::class)
internal fun KParameter.isDataFetchingEnvironment() = this.type.classifier == DataFetchingEnvironment::class
@Throws(CouldNotGetNameOfKParameterException::class)
internal fun KParameter.getName(): String =
this.getGraphQLName() ?: this.name ?: throw CouldNotGetNameOfKParameterException(this)
internal fun KType.getKClass() = this.jvmErasure
internal fun KType.getJavaClass(): Class<*> = this.getKClass().java
internal fun KType.isSubclassOf(kClass: KClass<*>) = this.getKClass().isSubclassOf(kClass)
internal fun KType.isList() = this.isSubclassOf(List::class)
internal fun KType.isArray() = this.getJavaClass().isArray
internal fun KType.isOptionalInputType() = this.isSubclassOf(OptionalInput::class)
@Throws(InvalidWrappedTypeException::class)
internal fun KType.getTypeOfFirstArgument(): KType =
this.arguments.firstOrNull()?.type ?: throw InvalidWrappedTypeException(this)
internal fun KType.getWrappedType(): KType {
val primitiveClass = primitiveArrayTypes[this.getKClass()]
return when {
primitiveClass != null -> primitiveClass.createType()
else -> this.getTypeOfFirstArgument()
}
}
private val primitiveArrayTypes = mapOf(
IntArray::class to Int::class,
LongArray::class to Long::class,
ShortArray::class to Short::class,
FloatArray::class to Float::class,
DoubleArray::class to Double::class,
CharArray::class to Char::class,
BooleanArray::class to Boolean::class
)
internal fun KAnnotatedElement.getGraphQLName(): String? = this.findAnnotation<GraphQLName>()?.value JacksonKotlinDataFetcherFactoryProvider import com.expediagroup.graphql.generator.execution.SimpleKotlinDataFetcherFactoryProvider
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import graphql.schema.DataFetcherFactory
import kotlin.reflect.KFunction
class JacksonKotlinDataFetcherFactoryProvider(private val objectMapper: ObjectMapper)
: SimpleKotlinDataFetcherFactoryProvider() {
override fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>) = DataFetcherFactory {
JacksonFunctionDataFetcher(
target = target,
fn = kFunction,
objectMapper = objectMapper
)
}
} GraphQlConfig config = SchemaGeneratorConfig(
dataFetcherFactoryProvider = JacksonKotlinDataFetcherFactoryProvider(jsonObjectMapper),
), |
I upgraded one of our project with the copied (dirty) workaround from the 5.x branch. We are not happy with the solution. please let me know if I can contribute something. There are a lot of options 1 2 3 4 if none of them is an option please close the draft pr. |
While it is unfortunate that dropping Jackson broke the functionality for you, we never targeted full Java compatibility (due to the lack of nullability information from Java POJOs as they are treated as system types). Creating custom We dropped Jackson dependency from
Since those POJOs are system types, all properties on those objects are treated as required so we need an all-arg constructor. If there are multiple constructors we would need additional logic to figure out the all-arg constructor. What is the order of the constructors? If we match constructors by number of input fields but input is missing some required fields -> how do we determine that required fields were missing? What if there are multiple constructors that take same number of arguments? Enforcing single all-arg constructor rule is the simplest workaround to enable usage of Java POJOs without implementing tons of custom logic to support that use case (we never targeted full Java interop). |
📝 Description
@dariuszkuc @samuelAndalon
it's only a draft to evaluate if this way is an option as well. I added a function for java object mapping. The default implementation results
null
but it can overwritten in customFunctionDataFetcher
. I updated my little playground project as well.I'm going to add tests as well. actually it is only a draft.
🔗 Related Issues
#1515