From d4f29ec47730fcc31d26e2d2254db24901298152 Mon Sep 17 00:00:00 2001 From: Oryan Date: Sun, 18 Oct 2026 20:05:43 -0400 Subject: [PATCH] Resolve type variables in resolver parameter types --- .../kickstart/tools/GenericTypeResolver.kt | 2 +- .../tools/resolver/MethodFieldResolver.kt | 3 +- .../kickstart/tools/GenericResolverTest.kt | 42 +++++++++++++++++++ .../tools/UnmappedInputFieldsTest.kt | 27 ++++++------ 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt b/src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt index 66a8762c..13da3e3f 100644 --- a/src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt +++ b/src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt @@ -52,7 +52,7 @@ internal class GenericTypeResolver(private val containingType: JavaType, private } } - private fun resolveTypeVariables(type: JavaType, resolving: Set> = emptySet()): JavaType { + fun resolveTypeVariables(type: JavaType, resolving: Set> = emptySet()): JavaType { return when (type) { is ParameterizedType -> { val actualTypeArguments = type.actualTypeArguments.map { resolveTypeVariables(it, resolving) }.toTypedArray() diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt index 4f954e1e..2d721bc8 100644 --- a/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt +++ b/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt @@ -74,7 +74,6 @@ internal class MethodFieldResolver( this.field.inputValueDefinitions.forEachIndexed { index, definition -> val parameterType = this.getMethodParameterType(index) - ?.apply { typeResolver.getRawClass(this) } ?: throw ResolverError("Missing method type at position ${this.getJavaMethodParameterIndex(index)}, this is most likely a bug with graphql-java-tools") val isNonNull = definition.type is NonNullType @@ -206,7 +205,7 @@ internal class MethodFieldResolver( val parameters = method.parameterTypes return if (parameters.size > methodIndex) { - method.genericParameterTypes[methodIndex] + typeResolver.resolveTypeVariables(method.genericParameterTypes[methodIndex]) } else { null } diff --git a/src/test/kotlin/graphql/kickstart/tools/GenericResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/GenericResolverTest.kt index faf96990..08ba7b15 100644 --- a/src/test/kotlin/graphql/kickstart/tools/GenericResolverTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/GenericResolverTest.kt @@ -70,6 +70,48 @@ class GenericResolverTest { class CarResolver : FooGraphQLResolver() + @Test + fun `inherited methods taking a type variable of their resolver are resolved`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + generic(input: GreetingInput!): String! + plain(input: GreetingInput!): String! + } + + input GreetingInput { + name: String! + } + """) + .resolvers(GreetingQuery()) + .build() + .makeExecutableSchema() + .assertEveryFieldResolves() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + generic(input: { name: "a" }) + plain(input: { name: "b" }) + } + """ + } + + assertEquals(data, mapOf("generic" to "generic got GreetingInput(name=a)", "plain" to "plain got GreetingInput(name=b)")) + } + + data class GreetingInput(val name: String) + + abstract class InputResolver : GraphQLQueryResolver { + fun generic(input: I): String = "generic got $input" + } + + class GreetingQuery : InputResolver() { + fun plain(input: GreetingInput): String = "plain got $input" + } + @Test fun `star projected resolvers are applied to parameterized data classes`() { val gql = GraphQL.newGraphQL(pageSchema(PageResolver())).build() diff --git a/src/test/kotlin/graphql/kickstart/tools/UnmappedInputFieldsTest.kt b/src/test/kotlin/graphql/kickstart/tools/UnmappedInputFieldsTest.kt index 949e252d..fe713dd8 100644 --- a/src/test/kotlin/graphql/kickstart/tools/UnmappedInputFieldsTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/UnmappedInputFieldsTest.kt @@ -328,7 +328,7 @@ class UnmappedInputFieldsTest { } @Test - fun `arguments are matched to their parameters past sources, environments and continuations`() { + fun `arguments are matched to their parameters past sources, environments and continuations, and through type variables`() { val error = assertThrows(SchemaError::class.java) { SchemaParser.newParser() .schemaString( @@ -337,6 +337,7 @@ class UnmappedInputFieldsTest { owner: Owner withEnvironment(input: EnvironmentInput!): String suspending(input: SuspendInput!): String + generic(input: GenericInput!): String } type Owner { @@ -354,6 +355,10 @@ class UnmappedInputFieldsTest { input ChildInput { name: String } + + input GenericInput { + name: String + } """) .resolvers(object : GraphQLQueryResolver { fun owner(): Owner = Owner() @@ -362,7 +367,7 @@ class UnmappedInputFieldsTest { suspend fun suspending(input: SuspendInput): String? = input.nmae }, object : GraphQLResolver { fun child(owner: Owner, id: String, input: ChildInput): String? = input.nmae - }) + }, GenericInputResolver()) .options(failOptions().build()) .build() .makeExecutableSchema() @@ -371,7 +376,8 @@ class UnmappedInputFieldsTest { assertEquals(error.message, listOf( unmapped("ChildInput.name", ChildInput::class.java, "argument 'input' of field 'Owner.child'", "nmae"), unmapped("EnvironmentInput.name", EnvironmentInput::class.java, "argument 'input' of field 'Query.withEnvironment'", "nmae"), - unmapped("SuspendInput.name", SuspendInput::class.java, "argument 'input' of field 'Query.suspending'", "nmae") + unmapped("SuspendInput.name", SuspendInput::class.java, "argument 'input' of field 'Query.suspending'", "nmae"), + unmapped("GenericInput.name", GenericInput::class.java, "argument 'input' of field 'Query.generic'", "nmae") ).joinToString("\n")) } @@ -690,7 +696,6 @@ class UnmappedInputFieldsTest { injected(input: InjectedInput!): String builder(input: BuilderTypoInput!): String unwrapped(input: KotlinUnwrappedInput!): String - generic(input: GenericInput!): String } input InjectedInput { @@ -705,27 +710,19 @@ class UnmappedInputFieldsTest { name: String street: String } - - input GenericInput { - name: String - } """) .resolvers(object : GraphQLQueryResolver { fun injected(input: InjectedInput): String? = input.nmae fun builder(input: BuilderTypoInput): String? = input.name fun unwrapped(input: KotlinUnwrappedInput): String = "${input.name} ${input.address?.stret}" - }, GenericInputResolver()) + }) .options(failOptions() .objectMapperConfigurer { mapper, _ -> mapper.setInjectableValues(InjectableValues.Std().addValue("tenant", "acme")) } .build()) .build() .makeExecutableSchema() - .assertEveryFieldResolves(except = setOf( - // the input classes misspell name on purpose - "Query.injected", "Query.builder", - // TODO query generic once the type variable of its inherited parameter is resolved (#880) - "Query.generic" - )) + // the input classes misspell name on purpose + .assertEveryFieldResolves(except = setOf("Query.injected", "Query.builder")) val result = GraphQL.newGraphQL(schema).build().execute( """