From 2bfca9a22e214d4e8c9283fd93c99ed2c22b1ebd Mon Sep 17 00:00:00 2001 From: Oryan Date: Sat, 3 Oct 2026 13:54:30 -0400 Subject: [PATCH 1/3] Resolve type variables against the most specific type Type variables in field and method types were resolved against the declaring type, which only knows the arguments passed in by its direct subclass. When those were type variables too (e.g. Item extends BaseItem extends AbstractItem), the fallback matched them by name against the first parameterized superclass. That recursed forever when the names repeated, failed with "No type variable found" when they differed, and silently picked the wrong argument when a subclass reordered them. Resolve them against the most specific type instead, which binds the type variables of all its supertypes, or its owner types for those of outer classes, and drop the name matching. Variables in wildcard bounds (e.g. List, which Kotlin emits for List parameters) are resolved too. A variable that can't be resolved, such as one declared by a generic method, fails with a clear error. One bound to a type containing itself, which can only leak out of a raw type, is erased like the raw type would instead of being expanded endlessly. Generic types returned by methods of a generic superclass now keep their resolved arguments, as fields already did. So two subclasses binding them differently can't share one GraphQL type anymore, like direct references to Page and Page. The "Two different classes" error now prints the parameterized types to show the difference. Fixes #460 Fixes #218 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../graphql/kickstart/tools/GenericType.kt | 62 +-- .../kickstart/tools/SchemaClassScanner.kt | 4 +- .../kickstart/tools/GenericInputTypesTest.kt | 50 ++ .../kickstart/tools/GenericObjectTypesTest.kt | 489 ++++++++++++++++++ .../kickstart/tools/RawGenericFixtures.java | 37 ++ 5 files changed, 597 insertions(+), 45 deletions(-) create mode 100644 src/test/kotlin/graphql/kickstart/tools/RawGenericFixtures.java diff --git a/src/main/kotlin/graphql/kickstart/tools/GenericType.kt b/src/main/kotlin/graphql/kickstart/tools/GenericType.kt index 5d681f27..90a53d32 100644 --- a/src/main/kotlin/graphql/kickstart/tools/GenericType.kt +++ b/src/main/kotlin/graphql/kickstart/tools/GenericType.kt @@ -4,7 +4,6 @@ import com.fasterxml.classmate.ResolvedType import graphql.kickstart.tools.util.JavaType import graphql.kickstart.tools.util.ParameterizedTypeImpl import graphql.kickstart.tools.util.Primitives -import graphql.kickstart.tools.util.unwrap import org.apache.commons.lang3.reflect.TypeUtils import java.lang.reflect.ParameterizedType import java.lang.reflect.TypeVariable @@ -97,14 +96,7 @@ internal open class GenericType(protected val mostSpecificType: JavaType, protec val unwrapsTo = genericType.schemaWrapper.invoke(typeArguments[genericType.index]) return unwrapGenericType(unwrapsTo) } - is TypeVariable<*> -> { - val parameterizedDeclaringType = parameterizedDeclaringTypeOrSuperType(declaringType) - if (parameterizedDeclaringType != null) { - unwrapGenericType(parameterizedDeclaringType, type) - } else { - error("Could not resolve type variable '${TypeUtils.toLongString(type)}' because declaring type is not parameterized: ${TypeUtils.toString(declaringType)}") - } - } + is TypeVariable<*> -> error("Could not resolve type variable '${TypeUtils.toLongString(type)}' of ${TypeUtils.toString(declaringType)} relative to ${TypeUtils.toString(mostSpecificType)}") is WildcardType -> type.upperBounds.firstOrNull() ?: error("Unable to unwrap type, wildcard has no upper bound: $type") is Class<*> -> if (type.isPrimitive) Primitives.wrap(type) else type @@ -112,36 +104,16 @@ internal open class GenericType(protected val mostSpecificType: JavaType, protec } } - private fun parameterizedDeclaringTypeOrSuperType(declaringType: JavaType): ParameterizedType? = - if (declaringType is ParameterizedType) { - declaringType - } else { - val superclass = declaringType.unwrap().genericSuperclass - if (superclass != null) { - parameterizedDeclaringTypeOrSuperType(superclass) - } else { - null - } - } - - private fun unwrapGenericType(declaringType: ParameterizedType, type: TypeVariable<*>): JavaType { - val rawClass = getRawClass(mostSpecificType) - val arguments = TypeUtils.determineTypeArguments(rawClass, declaringType) - val matchingType = arguments - .filter { it.key.name == type.name } - .values - .firstOrNull() - ?: error("No type variable found for: ${TypeUtils.toLongString(type)}") - - return unwrapGenericType(matchingType) - } - - private fun replaceTypeVariable(type: JavaType): JavaType { + private fun replaceTypeVariable(type: JavaType, resolving: Set> = emptySet()): JavaType { return when (type) { is ParameterizedType -> { - val actualTypeArguments = type.actualTypeArguments.map { replaceTypeVariable(it) }.toTypedArray() - ParameterizedTypeImpl(type.rawType as Class<*>, actualTypeArguments, type.ownerType) + val actualTypeArguments = type.actualTypeArguments.map { replaceTypeVariable(it, resolving) }.toTypedArray() + ParameterizedTypeImpl(type.rawType as Class<*>, actualTypeArguments, type.ownerType?.let { replaceTypeVariable(it, resolving) }) } + is WildcardType -> TypeUtils.wildcardType() + .withUpperBounds(*type.upperBounds.map { replaceTypeVariable(it, resolving) }.toTypedArray()) + .withLowerBounds(*type.lowerBounds.map { replaceTypeVariable(it, resolving) }.toTypedArray()) + .build() is ResolvedType -> { if (type.typeParameters.isEmpty()) { type.erasedType @@ -152,14 +124,18 @@ internal open class GenericType(protected val mostSpecificType: JavaType, protec } is TypeVariable<*> -> { val genericDeclaration = type.genericDeclaration - if (declaringType is ParameterizedType && genericDeclaration is Class<*>) { - // keep the full type argument (e.g. List) rather than its raw class so nested generics aren't lost - TypeUtils.getTypeArguments(declaringType, genericDeclaration)?.get(type) - ?.takeIf { it != type } - ?.let { replaceTypeVariable(it) } + when { + // only a variable leaked from a raw type can be bound to a type containing itself (e.g. T -> List), + // erase it like the raw type does instead of expanding it forever + type in resolving -> TypeUtils.getRawType(type.bounds.first(), null) ?: Any::class.java + // the most specific type binds the variables of its supertypes and its owner types those of outer classes, + // the declaring type may not + genericDeclaration is Class<*> -> generateSequence(mostSpecificType) { (it as? ParameterizedType)?.ownerType } + .firstNotNullOfOrNull { TypeUtils.getTypeArguments(it, genericDeclaration)?.get(type) } + // keep the full type argument (e.g. List) rather than its raw class so nested generics aren't lost + ?.let { replaceTypeVariable(it, resolving + type) } ?: type - } else { - type + else -> type } } else -> { diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index 8d678863..19dede53 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -360,7 +360,7 @@ internal class SchemaClassScanner( if (java.util.Map::class.java.isAssignableFrom(javaType.unwrap())) { throw SchemaClassScannerError("Two different property map classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType}:\n| ${reference.getDescription()}") } - throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType.unwrap()}:\n| ${reference.getDescription()}") + throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType.typeName}:\n| ${reference.getDescription()}") } } } @@ -469,7 +469,7 @@ internal class SchemaClassScanner( references.add(reference) } - fun joinReferences() = "- ${typeClass()}:\n| " + references.joinToString("\n| ") { it.getDescription() } + fun joinReferences() = "- ${javaType?.typeName}:\n| " + references.joinToString("\n| ") { it.getDescription() } fun hasResolverRef(): Boolean { references.filterIsInstance().forEach { reference -> diff --git a/src/test/kotlin/graphql/kickstart/tools/GenericInputTypesTest.kt b/src/test/kotlin/graphql/kickstart/tools/GenericInputTypesTest.kt index 2cf3f6dc..6856a07f 100644 --- a/src/test/kotlin/graphql/kickstart/tools/GenericInputTypesTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/GenericInputTypesTest.kt @@ -166,6 +166,39 @@ class GenericInputTypesTest { assertEquals(data["audit"], "why:op:1") } + @Test + fun `generic list fields inherited from parameterized superclasses are parsed`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + mutate(input: MutationInput!): String! + } + + input MutationInput { + items: [Item!]! + } + + input Item { + prop: String! + } + """) + .resolvers(InheritedListQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + mutate(input: { items: [{ prop: "a" }, { prop: "b" }] }) + } + """ + } + + assertEquals(data["mutate"], "a,b") + } + class QueryResolver : GraphQLQueryResolver { fun audit(input: AuditWrapper): String = "${input.operator}:${input.content?.id}" fun audits(input: AuditWrapper>): String = "${input.operator}:${input.content?.joinToString(",") { it.id.orEmpty() }}" @@ -184,6 +217,10 @@ class GenericInputTypesTest { fun audit(input: AuditRequest): String = "${input.reason}:${input.audit?.operator}:${input.audit?.content?.id}" } + class InheritedListQueryResolver : GraphQLQueryResolver { + fun mutate(input: MutationInput): String = input.items.orEmpty().joinToString(",") { it.prop.orEmpty() } + } + open class AuditWrapper { var content: T? = null var operator: String? = null @@ -196,6 +233,19 @@ class GenericInputTypesTest { var reason: String? = null } + abstract class GenericMutationInput { + @JvmField + var items: List? = null + } + + abstract class RenamedMutationInput : GenericMutationInput() + + class MutationInput : RenamedMutationInput() { + class Item { + var prop: String? = null + } + } + class LanguageInput { var id: String? = null } diff --git a/src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt b/src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt index c59e07a0..9d56d20f 100644 --- a/src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt @@ -1,6 +1,7 @@ package graphql.kickstart.tools import graphql.GraphQL +import org.junit.Assert.assertThrows import org.junit.Test class GenericObjectTypesTest { @@ -122,6 +123,380 @@ class GenericObjectTypesTest { )) } + @Test + fun `type variables passed through several superclasses are resolved for fields`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + item: Item! + holder: Holder! + } + + type Item { + id: ID! + } + + type Holder { + value: Owner! + } + + type Owner { + name: String! + } + """) + .resolvers(FieldItemQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + item { id } + holder { value { name } } + } + """ + } + + assertEquals(data["item"], mapOf("id" to "1")) + assertEquals(data["holder"], mapOf("value" to mapOf("name" to "owner"))) + } + + @Test + fun `type variables passed through several superclasses are resolved for getters`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + item: Item! + holder: Holder! + } + + type Item { + id: ID! + } + + type Holder { + value: Owner! + } + + type Owner { + name: String! + } + """) + .resolvers(GetterItemQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + item { id } + holder { value { name } } + } + """ + } + + assertEquals(data["item"], mapOf("id" to "2")) + assertEquals(data["holder"], mapOf("value" to mapOf("name" to "owner"))) + } + + @Test + fun `type variables renamed by superclasses are resolved`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + account: Account! + } + + type Account { + id: ID! + owner: Owner! + } + + type Owner { + name: String! + } + """) + .resolvers(AccountQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + account { + id + owner { name } + } + } + """ + } + + assertEquals(data["account"], mapOf("id" to "3", "owner" to mapOf("name" to "owner"))) + } + + @Test + fun `type variables reordered by a superclass are resolved`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + tuple: Tuple! + } + + type Tuple { + first: Owner! + second: Tag! + } + + type Owner { + name: String! + } + + type Tag { + label: String! + } + """) + .resolvers(TupleQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + tuple { + first { name } + second { label } + } + } + """ + } + + assertEquals(data["tuple"], mapOf("first" to mapOf("name" to "owner"), "second" to mapOf("label" to "tag"))) + } + + @Test + fun `type variables nested in types inherited from a superclass are resolved`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + owners: OwnerConnection! + } + + type OwnerConnection { + edges: [OwnerEdge!]! + } + + type OwnerEdge { + node: Owner! + } + + type Owner { + name: String! + } + """) + .resolvers(ConnectionQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + owners { + edges { + node { name } + } + } + } + """ + } + + assertEquals(data["owners"], mapOf("edges" to listOf(mapOf("node" to mapOf("name" to "owner"))))) + } + + @Test + fun `type variables leaked from raw types don't overflow the stack`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + tree: Tree! + } + + type Tree { + grouped: GroupedTree! + async: AsyncTree! + } + + type GroupedTree { + value: [String!]! + } + + type AsyncTree { + value: String! + } + """) + .resolvers(RawGenericFixtures.QueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + tree { + grouped { value } + async { value } + } + } + """ + } + + assertEquals(data["tree"], mapOf("grouped" to mapOf("value" to listOf("leaf")), "async" to mapOf("value" to "leaf"))) + } + + @Test + fun `type variables in wildcard bounds are resolved`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + owners: OwnerList! + } + + type OwnerList { + items: [Owner]! + } + + type Owner { + name: String! + } + """) + .resolvers(WildcardQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + owners { + items { name } + } + } + """ + } + + assertEquals(data["owners"], mapOf("items" to listOf(mapOf("name" to "owner")))) + } + + @Test + fun `type variables of outer classes are resolved`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + owners: OwnerListing! + } + + type OwnerListing { + entries: [OwnerEntry!]! + } + + type OwnerEntry { + position: Int! + value: Owner! + } + + type Owner { + name: String! + } + """) + .resolvers(ListingQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + owners { + entries { + position + value { name } + } + } + } + """ + } + + assertEquals(data["owners"], mapOf("entries" to listOf(mapOf("position" to 0, "value" to mapOf("name" to "owner"))))) + } + + @Test + fun `generic types bound differently by subclasses can't share a type`() { + val error = assertThrows(SchemaClassScannerError::class.java) { + SchemaParser.newParser() + .schemaString( + """ + type Query { + owners: OwnerPage! + tags: TagPage! + } + + type OwnerPage { + meta: Meta! + } + + type TagPage { + meta: Meta! + } + + type Meta { + total: Int! + } + """) + .resolvers(MetaQueryResolver()) + .build() + .makeExecutableSchema() + } + + val message = error.message.orEmpty() + assert(message.startsWith("Two different classes used for type Meta")) { message } + assert(message.contains("${Meta::class.java.name}<${Owner::class.java.name}>")) { message } + assert(message.contains("${Meta::class.java.name}<${Tag::class.java.name}>")) { message } + } + + @Test + fun `type variables of generic methods can't be resolved`() { + val error = assertThrows(IllegalStateException::class.java) { + SchemaParser.newParser() + .schemaString( + """ + type Query { + owner: Owner! + } + + type Owner { + name: String! + } + """) + .resolvers(GenericMethodQueryResolver()) + .build() + .makeExecutableSchema() + } + + assert(error.message.orEmpty().startsWith("Could not resolve type variable")) { error.message.orEmpty() } + } + class EntityMatchQueryResolver : GraphQLQueryResolver { fun test(): TestClass = TestClass( match = EntityMatch("x y", listOf("x", "y")), @@ -152,4 +527,118 @@ class GenericObjectTypesTest { class Human(override val name: String, val homePlanet: String) : Character class Droid(override val name: String, val primaryFunction: String) : Character + + class FieldItemQueryResolver : GraphQLQueryResolver { + fun item(): FieldItem = FieldItem(1) + fun holder(): FieldHolder = FieldHolder(Owner("owner")) + } + + open class AbstractFieldItem(@JvmField val id: T) + + open class BaseFieldItem(id: T) : AbstractFieldItem(id) + + class FieldItem(id: Long) : BaseFieldItem(id) + + open class BaseFieldHolder(@JvmField val value: U) + + class FieldHolder(value: T) : BaseFieldHolder(value) + + class GetterItemQueryResolver : GraphQLQueryResolver { + fun item(): GetterItem = GetterItem(2) + fun holder(): OwnerHolder = OwnerHolder(Owner("owner")) + } + + open class AbstractGetterItem(val id: T) + + open class BaseGetterItem(id: T) : AbstractGetterItem(id) + + class GetterItem(id: Long) : BaseGetterItem(id) + + open class AbstractHolder(val value: T) + + open class BaseHolder(value: T) : AbstractHolder(value) + + class OwnerHolder(value: Owner) : BaseHolder(value) + + class AccountQueryResolver : GraphQLQueryResolver { + fun account(): Account = Account(3, Owner("owner")) + } + + interface Identifiable { + val id: I + } + + abstract class OwnedEntity(override val id: K, val owner: R) : Identifiable + + abstract class AuditableEntity(id: E, owner: O) : OwnedEntity(id, owner) + + class Account(id: Long, owner: Owner) : AuditableEntity(id, owner) + + class TupleQueryResolver : GraphQLQueryResolver { + fun tuple(): OwnerTagTuple = OwnerTagTuple(Owner("owner"), Tag("tag")) + } + + open class Tuple(val first: A, val second: B) + + open class ReversedTuple(first: B, second: A) : Tuple(first, second) + + class OwnerTagTuple(first: Owner, second: Tag) : ReversedTuple(first, second) + + class ConnectionQueryResolver : GraphQLQueryResolver { + fun owners(): OwnerConnection = OwnerConnection(listOf(Edge(Owner("owner")))) + } + + abstract class Connection(val edges: List>) + + class Edge(val node: T) + + class OwnerConnection(edges: List>) : Connection(edges) + + class WildcardQueryResolver : GraphQLQueryResolver { + fun owners(): OwnerList = OwnerList(listOf(Owner("owner"))) + } + + abstract class WildcardList(val items: List<@JvmWildcard T>) + + class OwnerList(items: List) : WildcardList(items) + + class ListingQueryResolver : GraphQLQueryResolver { + fun owners(): OwnerListing = OwnerListing(listOf(Owner("owner"))) + } + + abstract class Listing(private val values: List) { + val entries: List + get() = values.mapIndexed { position, value -> Entry(position, value) } + + inner class Entry(val position: Int, val value: T) + } + + class OwnerListing(values: List) : Listing(values) + + class MetaQueryResolver : GraphQLQueryResolver { + fun owners(): OwnerPage = OwnerPage() + fun tags(): TagPage = TagPage() + } + + class Meta(val total: Int) + + abstract class MetaPage { + val meta: Meta = Meta(0) + } + + class OwnerPage : MetaPage() + + class TagPage : MetaPage() + + open class GenericMethodBase + + // the class binds its own T, which doesn't make the method's T resolvable + class GenericMethodQueryResolver : GenericMethodBase(), GraphQLQueryResolver { + @Suppress("UNCHECKED_CAST") + fun owner(): T = Owner("owner") as T + } + + class Owner(val name: String) + + class Tag(val label: String) } diff --git a/src/test/kotlin/graphql/kickstart/tools/RawGenericFixtures.java b/src/test/kotlin/graphql/kickstart/tools/RawGenericFixtures.java new file mode 100644 index 00000000..d01b3076 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/RawGenericFixtures.java @@ -0,0 +1,37 @@ +package graphql.kickstart.tools; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +// Raw types can't be expressed in Kotlin, so these fixtures must stay in Java. +public class RawGenericFixtures { + + public static class QueryResolver implements GraphQLQueryResolver { + + @SuppressWarnings({"rawtypes", "unused"}) + public Tree tree() { + return new Tree<>("leaf"); + } + } + + public static class Tree { + private final T value; + + public Tree(T value) { + this.value = value; + } + + public T getValue() { + return value; + } + + // returned from a raw Tree, these are trees whose T is bound to a type containing that same, unbound T + public Tree> getGrouped() { + return new Tree<>(List.of(value)); + } + + public Tree> getAsync() { + return new Tree<>(CompletableFuture.completedFuture(value)); + } + } +} From 3e15799dd4b23e122aa05b34c9357acfca9373fb Mon Sep 17 00:00:00 2001 From: Oryan Date: Sat, 3 Oct 2026 16:43:03 -0400 Subject: [PATCH 2/3] Move inherited type variable tests to their own class Co-Authored-By: Claude Opus 5.5 (1M context) --- .../kickstart/tools/GenericObjectTypesTest.kt | 489 ----------------- .../tools/InheritedTypeVariablesTest.kt | 496 ++++++++++++++++++ 2 files changed, 496 insertions(+), 489 deletions(-) create mode 100644 src/test/kotlin/graphql/kickstart/tools/InheritedTypeVariablesTest.kt diff --git a/src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt b/src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt index 9d56d20f..c59e07a0 100644 --- a/src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt @@ -1,7 +1,6 @@ package graphql.kickstart.tools import graphql.GraphQL -import org.junit.Assert.assertThrows import org.junit.Test class GenericObjectTypesTest { @@ -123,380 +122,6 @@ class GenericObjectTypesTest { )) } - @Test - fun `type variables passed through several superclasses are resolved for fields`() { - val schema = SchemaParser.newParser() - .schemaString( - """ - type Query { - item: Item! - holder: Holder! - } - - type Item { - id: ID! - } - - type Holder { - value: Owner! - } - - type Owner { - name: String! - } - """) - .resolvers(FieldItemQueryResolver()) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - query { - item { id } - holder { value { name } } - } - """ - } - - assertEquals(data["item"], mapOf("id" to "1")) - assertEquals(data["holder"], mapOf("value" to mapOf("name" to "owner"))) - } - - @Test - fun `type variables passed through several superclasses are resolved for getters`() { - val schema = SchemaParser.newParser() - .schemaString( - """ - type Query { - item: Item! - holder: Holder! - } - - type Item { - id: ID! - } - - type Holder { - value: Owner! - } - - type Owner { - name: String! - } - """) - .resolvers(GetterItemQueryResolver()) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - query { - item { id } - holder { value { name } } - } - """ - } - - assertEquals(data["item"], mapOf("id" to "2")) - assertEquals(data["holder"], mapOf("value" to mapOf("name" to "owner"))) - } - - @Test - fun `type variables renamed by superclasses are resolved`() { - val schema = SchemaParser.newParser() - .schemaString( - """ - type Query { - account: Account! - } - - type Account { - id: ID! - owner: Owner! - } - - type Owner { - name: String! - } - """) - .resolvers(AccountQueryResolver()) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - query { - account { - id - owner { name } - } - } - """ - } - - assertEquals(data["account"], mapOf("id" to "3", "owner" to mapOf("name" to "owner"))) - } - - @Test - fun `type variables reordered by a superclass are resolved`() { - val schema = SchemaParser.newParser() - .schemaString( - """ - type Query { - tuple: Tuple! - } - - type Tuple { - first: Owner! - second: Tag! - } - - type Owner { - name: String! - } - - type Tag { - label: String! - } - """) - .resolvers(TupleQueryResolver()) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - query { - tuple { - first { name } - second { label } - } - } - """ - } - - assertEquals(data["tuple"], mapOf("first" to mapOf("name" to "owner"), "second" to mapOf("label" to "tag"))) - } - - @Test - fun `type variables nested in types inherited from a superclass are resolved`() { - val schema = SchemaParser.newParser() - .schemaString( - """ - type Query { - owners: OwnerConnection! - } - - type OwnerConnection { - edges: [OwnerEdge!]! - } - - type OwnerEdge { - node: Owner! - } - - type Owner { - name: String! - } - """) - .resolvers(ConnectionQueryResolver()) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - query { - owners { - edges { - node { name } - } - } - } - """ - } - - assertEquals(data["owners"], mapOf("edges" to listOf(mapOf("node" to mapOf("name" to "owner"))))) - } - - @Test - fun `type variables leaked from raw types don't overflow the stack`() { - val schema = SchemaParser.newParser() - .schemaString( - """ - type Query { - tree: Tree! - } - - type Tree { - grouped: GroupedTree! - async: AsyncTree! - } - - type GroupedTree { - value: [String!]! - } - - type AsyncTree { - value: String! - } - """) - .resolvers(RawGenericFixtures.QueryResolver()) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - query { - tree { - grouped { value } - async { value } - } - } - """ - } - - assertEquals(data["tree"], mapOf("grouped" to mapOf("value" to listOf("leaf")), "async" to mapOf("value" to "leaf"))) - } - - @Test - fun `type variables in wildcard bounds are resolved`() { - val schema = SchemaParser.newParser() - .schemaString( - """ - type Query { - owners: OwnerList! - } - - type OwnerList { - items: [Owner]! - } - - type Owner { - name: String! - } - """) - .resolvers(WildcardQueryResolver()) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - query { - owners { - items { name } - } - } - """ - } - - assertEquals(data["owners"], mapOf("items" to listOf(mapOf("name" to "owner")))) - } - - @Test - fun `type variables of outer classes are resolved`() { - val schema = SchemaParser.newParser() - .schemaString( - """ - type Query { - owners: OwnerListing! - } - - type OwnerListing { - entries: [OwnerEntry!]! - } - - type OwnerEntry { - position: Int! - value: Owner! - } - - type Owner { - name: String! - } - """) - .resolvers(ListingQueryResolver()) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - query { - owners { - entries { - position - value { name } - } - } - } - """ - } - - assertEquals(data["owners"], mapOf("entries" to listOf(mapOf("position" to 0, "value" to mapOf("name" to "owner"))))) - } - - @Test - fun `generic types bound differently by subclasses can't share a type`() { - val error = assertThrows(SchemaClassScannerError::class.java) { - SchemaParser.newParser() - .schemaString( - """ - type Query { - owners: OwnerPage! - tags: TagPage! - } - - type OwnerPage { - meta: Meta! - } - - type TagPage { - meta: Meta! - } - - type Meta { - total: Int! - } - """) - .resolvers(MetaQueryResolver()) - .build() - .makeExecutableSchema() - } - - val message = error.message.orEmpty() - assert(message.startsWith("Two different classes used for type Meta")) { message } - assert(message.contains("${Meta::class.java.name}<${Owner::class.java.name}>")) { message } - assert(message.contains("${Meta::class.java.name}<${Tag::class.java.name}>")) { message } - } - - @Test - fun `type variables of generic methods can't be resolved`() { - val error = assertThrows(IllegalStateException::class.java) { - SchemaParser.newParser() - .schemaString( - """ - type Query { - owner: Owner! - } - - type Owner { - name: String! - } - """) - .resolvers(GenericMethodQueryResolver()) - .build() - .makeExecutableSchema() - } - - assert(error.message.orEmpty().startsWith("Could not resolve type variable")) { error.message.orEmpty() } - } - class EntityMatchQueryResolver : GraphQLQueryResolver { fun test(): TestClass = TestClass( match = EntityMatch("x y", listOf("x", "y")), @@ -527,118 +152,4 @@ class GenericObjectTypesTest { class Human(override val name: String, val homePlanet: String) : Character class Droid(override val name: String, val primaryFunction: String) : Character - - class FieldItemQueryResolver : GraphQLQueryResolver { - fun item(): FieldItem = FieldItem(1) - fun holder(): FieldHolder = FieldHolder(Owner("owner")) - } - - open class AbstractFieldItem(@JvmField val id: T) - - open class BaseFieldItem(id: T) : AbstractFieldItem(id) - - class FieldItem(id: Long) : BaseFieldItem(id) - - open class BaseFieldHolder(@JvmField val value: U) - - class FieldHolder(value: T) : BaseFieldHolder(value) - - class GetterItemQueryResolver : GraphQLQueryResolver { - fun item(): GetterItem = GetterItem(2) - fun holder(): OwnerHolder = OwnerHolder(Owner("owner")) - } - - open class AbstractGetterItem(val id: T) - - open class BaseGetterItem(id: T) : AbstractGetterItem(id) - - class GetterItem(id: Long) : BaseGetterItem(id) - - open class AbstractHolder(val value: T) - - open class BaseHolder(value: T) : AbstractHolder(value) - - class OwnerHolder(value: Owner) : BaseHolder(value) - - class AccountQueryResolver : GraphQLQueryResolver { - fun account(): Account = Account(3, Owner("owner")) - } - - interface Identifiable { - val id: I - } - - abstract class OwnedEntity(override val id: K, val owner: R) : Identifiable - - abstract class AuditableEntity(id: E, owner: O) : OwnedEntity(id, owner) - - class Account(id: Long, owner: Owner) : AuditableEntity(id, owner) - - class TupleQueryResolver : GraphQLQueryResolver { - fun tuple(): OwnerTagTuple = OwnerTagTuple(Owner("owner"), Tag("tag")) - } - - open class Tuple(val first: A, val second: B) - - open class ReversedTuple(first: B, second: A) : Tuple(first, second) - - class OwnerTagTuple(first: Owner, second: Tag) : ReversedTuple(first, second) - - class ConnectionQueryResolver : GraphQLQueryResolver { - fun owners(): OwnerConnection = OwnerConnection(listOf(Edge(Owner("owner")))) - } - - abstract class Connection(val edges: List>) - - class Edge(val node: T) - - class OwnerConnection(edges: List>) : Connection(edges) - - class WildcardQueryResolver : GraphQLQueryResolver { - fun owners(): OwnerList = OwnerList(listOf(Owner("owner"))) - } - - abstract class WildcardList(val items: List<@JvmWildcard T>) - - class OwnerList(items: List) : WildcardList(items) - - class ListingQueryResolver : GraphQLQueryResolver { - fun owners(): OwnerListing = OwnerListing(listOf(Owner("owner"))) - } - - abstract class Listing(private val values: List) { - val entries: List - get() = values.mapIndexed { position, value -> Entry(position, value) } - - inner class Entry(val position: Int, val value: T) - } - - class OwnerListing(values: List) : Listing(values) - - class MetaQueryResolver : GraphQLQueryResolver { - fun owners(): OwnerPage = OwnerPage() - fun tags(): TagPage = TagPage() - } - - class Meta(val total: Int) - - abstract class MetaPage { - val meta: Meta = Meta(0) - } - - class OwnerPage : MetaPage() - - class TagPage : MetaPage() - - open class GenericMethodBase - - // the class binds its own T, which doesn't make the method's T resolvable - class GenericMethodQueryResolver : GenericMethodBase(), GraphQLQueryResolver { - @Suppress("UNCHECKED_CAST") - fun owner(): T = Owner("owner") as T - } - - class Owner(val name: String) - - class Tag(val label: String) } diff --git a/src/test/kotlin/graphql/kickstart/tools/InheritedTypeVariablesTest.kt b/src/test/kotlin/graphql/kickstart/tools/InheritedTypeVariablesTest.kt new file mode 100644 index 00000000..a51c6030 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/InheritedTypeVariablesTest.kt @@ -0,0 +1,496 @@ +package graphql.kickstart.tools + +import graphql.GraphQL +import org.junit.Assert.assertThrows +import org.junit.Test + +class InheritedTypeVariablesTest { + + @Test + fun `type variables passed through several superclasses are resolved for fields`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + item: Item! + holder: Holder! + } + + type Item { + id: ID! + } + + type Holder { + value: Owner! + } + + type Owner { + name: String! + } + """) + .resolvers(FieldItemQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + item { id } + holder { value { name } } + } + """ + } + + assertEquals(data["item"], mapOf("id" to "1")) + assertEquals(data["holder"], mapOf("value" to mapOf("name" to "owner"))) + } + + @Test + fun `type variables passed through several superclasses are resolved for getters`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + item: Item! + holder: Holder! + } + + type Item { + id: ID! + } + + type Holder { + value: Owner! + } + + type Owner { + name: String! + } + """) + .resolvers(GetterItemQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + item { id } + holder { value { name } } + } + """ + } + + assertEquals(data["item"], mapOf("id" to "2")) + assertEquals(data["holder"], mapOf("value" to mapOf("name" to "owner"))) + } + + @Test + fun `type variables renamed by superclasses are resolved`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + account: Account! + } + + type Account { + id: ID! + owner: Owner! + } + + type Owner { + name: String! + } + """) + .resolvers(AccountQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + account { + id + owner { name } + } + } + """ + } + + assertEquals(data["account"], mapOf("id" to "3", "owner" to mapOf("name" to "owner"))) + } + + @Test + fun `type variables reordered by a superclass are resolved`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + tuple: Tuple! + } + + type Tuple { + first: Owner! + second: Tag! + } + + type Owner { + name: String! + } + + type Tag { + label: String! + } + """) + .resolvers(TupleQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + tuple { + first { name } + second { label } + } + } + """ + } + + assertEquals(data["tuple"], mapOf("first" to mapOf("name" to "owner"), "second" to mapOf("label" to "tag"))) + } + + @Test + fun `type variables nested in types inherited from a superclass are resolved`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + owners: OwnerConnection! + } + + type OwnerConnection { + edges: [OwnerEdge!]! + } + + type OwnerEdge { + node: Owner! + } + + type Owner { + name: String! + } + """) + .resolvers(ConnectionQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + owners { + edges { + node { name } + } + } + } + """ + } + + assertEquals(data["owners"], mapOf("edges" to listOf(mapOf("node" to mapOf("name" to "owner"))))) + } + + @Test + fun `type variables leaked from raw types don't overflow the stack`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + tree: Tree! + } + + type Tree { + grouped: GroupedTree! + async: AsyncTree! + } + + type GroupedTree { + value: [String!]! + } + + type AsyncTree { + value: String! + } + """) + .resolvers(RawGenericFixtures.QueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + tree { + grouped { value } + async { value } + } + } + """ + } + + assertEquals(data["tree"], mapOf("grouped" to mapOf("value" to listOf("leaf")), "async" to mapOf("value" to "leaf"))) + } + + @Test + fun `type variables in wildcard bounds are resolved`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + owners: OwnerList! + } + + type OwnerList { + items: [Owner]! + } + + type Owner { + name: String! + } + """) + .resolvers(WildcardQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + owners { + items { name } + } + } + """ + } + + assertEquals(data["owners"], mapOf("items" to listOf(mapOf("name" to "owner")))) + } + + @Test + fun `type variables of outer classes are resolved`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + owners: OwnerListing! + } + + type OwnerListing { + entries: [OwnerEntry!]! + } + + type OwnerEntry { + position: Int! + value: Owner! + } + + type Owner { + name: String! + } + """) + .resolvers(ListingQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + owners { + entries { + position + value { name } + } + } + } + """ + } + + assertEquals(data["owners"], mapOf("entries" to listOf(mapOf("position" to 0, "value" to mapOf("name" to "owner"))))) + } + + @Test + fun `generic types bound differently by subclasses can't share a type`() { + val error = assertThrows(SchemaClassScannerError::class.java) { + SchemaParser.newParser() + .schemaString( + """ + type Query { + owners: OwnerPage! + tags: TagPage! + } + + type OwnerPage { + meta: Meta! + } + + type TagPage { + meta: Meta! + } + + type Meta { + total: Int! + } + """) + .resolvers(MetaQueryResolver()) + .build() + .makeExecutableSchema() + } + + val message = error.message.orEmpty() + assert(message.startsWith("Two different classes used for type Meta")) { message } + assert(message.contains("${Meta::class.java.name}<${Owner::class.java.name}>")) { message } + assert(message.contains("${Meta::class.java.name}<${Tag::class.java.name}>")) { message } + } + + @Test + fun `type variables of generic methods can't be resolved`() { + val error = assertThrows(IllegalStateException::class.java) { + SchemaParser.newParser() + .schemaString( + """ + type Query { + owner: Owner! + } + + type Owner { + name: String! + } + """) + .resolvers(GenericMethodQueryResolver()) + .build() + .makeExecutableSchema() + } + + assert(error.message.orEmpty().startsWith("Could not resolve type variable")) { error.message.orEmpty() } + } + + class FieldItemQueryResolver : GraphQLQueryResolver { + fun item(): FieldItem = FieldItem(1) + fun holder(): FieldHolder = FieldHolder(Owner("owner")) + } + + open class AbstractFieldItem(@JvmField val id: T) + + open class BaseFieldItem(id: T) : AbstractFieldItem(id) + + class FieldItem(id: Long) : BaseFieldItem(id) + + open class BaseFieldHolder(@JvmField val value: U) + + class FieldHolder(value: T) : BaseFieldHolder(value) + + class GetterItemQueryResolver : GraphQLQueryResolver { + fun item(): GetterItem = GetterItem(2) + fun holder(): OwnerHolder = OwnerHolder(Owner("owner")) + } + + open class AbstractGetterItem(val id: T) + + open class BaseGetterItem(id: T) : AbstractGetterItem(id) + + class GetterItem(id: Long) : BaseGetterItem(id) + + open class AbstractHolder(val value: T) + + open class BaseHolder(value: T) : AbstractHolder(value) + + class OwnerHolder(value: Owner) : BaseHolder(value) + + class AccountQueryResolver : GraphQLQueryResolver { + fun account(): Account = Account(3, Owner("owner")) + } + + interface Identifiable { + val id: I + } + + abstract class OwnedEntity(override val id: K, val owner: R) : Identifiable + + abstract class AuditableEntity(id: E, owner: O) : OwnedEntity(id, owner) + + class Account(id: Long, owner: Owner) : AuditableEntity(id, owner) + + class TupleQueryResolver : GraphQLQueryResolver { + fun tuple(): OwnerTagTuple = OwnerTagTuple(Owner("owner"), Tag("tag")) + } + + open class Tuple(val first: A, val second: B) + + open class ReversedTuple(first: B, second: A) : Tuple(first, second) + + class OwnerTagTuple(first: Owner, second: Tag) : ReversedTuple(first, second) + + class ConnectionQueryResolver : GraphQLQueryResolver { + fun owners(): OwnerConnection = OwnerConnection(listOf(Edge(Owner("owner")))) + } + + abstract class Connection(val edges: List>) + + class Edge(val node: T) + + class OwnerConnection(edges: List>) : Connection(edges) + + class WildcardQueryResolver : GraphQLQueryResolver { + fun owners(): OwnerList = OwnerList(listOf(Owner("owner"))) + } + + abstract class WildcardList(val items: List<@JvmWildcard T>) + + class OwnerList(items: List) : WildcardList(items) + + class ListingQueryResolver : GraphQLQueryResolver { + fun owners(): OwnerListing = OwnerListing(listOf(Owner("owner"))) + } + + abstract class Listing(private val values: List) { + val entries: List + get() = values.mapIndexed { position, value -> Entry(position, value) } + + inner class Entry(val position: Int, val value: T) + } + + class OwnerListing(values: List) : Listing(values) + + class MetaQueryResolver : GraphQLQueryResolver { + fun owners(): OwnerPage = OwnerPage() + fun tags(): TagPage = TagPage() + } + + class Meta(val total: Int) + + abstract class MetaPage { + val meta: Meta = Meta(0) + } + + class OwnerPage : MetaPage() + + class TagPage : MetaPage() + + open class GenericMethodBase + + // the class binds its own T, which doesn't make the method's T resolvable + class GenericMethodQueryResolver : GenericMethodBase(), GraphQLQueryResolver { + @Suppress("UNCHECKED_CAST") + fun owner(): T = Owner("owner") as T + } + + class Owner(val name: String) + + class Tag(val label: String) +} From 5b50b0928ae72193386d43a79b8dd3d94bbb025a Mon Sep 17 00:00:00 2001 From: Oryan Date: Mon, 5 Oct 2026 12:30:45 -0400 Subject: [PATCH 3/3] Share fixtures between inherited type variable tests Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tools/InheritedTypeVariablesTest.kt | 314 ++++-------------- 1 file changed, 62 insertions(+), 252 deletions(-) diff --git a/src/test/kotlin/graphql/kickstart/tools/InheritedTypeVariablesTest.kt b/src/test/kotlin/graphql/kickstart/tools/InheritedTypeVariablesTest.kt index a51c6030..e2452304 100644 --- a/src/test/kotlin/graphql/kickstart/tools/InheritedTypeVariablesTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/InheritedTypeVariablesTest.kt @@ -7,20 +7,22 @@ import org.junit.Test class InheritedTypeVariablesTest { @Test - fun `type variables passed through several superclasses are resolved for fields`() { + fun `type variables passed through several superclasses are resolved`() { val schema = SchemaParser.newParser() .schemaString( """ type Query { item: Item! - holder: Holder! + ownerItem: OwnerItem! } type Item { id: ID! + value: ID! } - type Holder { + type OwnerItem { + id: Owner! value: Owner! } @@ -28,7 +30,7 @@ class InheritedTypeVariablesTest { name: String! } """) - .resolvers(FieldItemQueryResolver()) + .resolvers(QueryResolver()) .build() .makeExecutableSchema() val gql = GraphQL.newGraphQL(schema).build() @@ -36,58 +38,21 @@ class InheritedTypeVariablesTest { val data = assertNoGraphQlErrors(gql) { """ query { - item { id } - holder { value { name } } - } - """ - } - - assertEquals(data["item"], mapOf("id" to "1")) - assertEquals(data["holder"], mapOf("value" to mapOf("name" to "owner"))) - } - - @Test - fun `type variables passed through several superclasses are resolved for getters`() { - val schema = SchemaParser.newParser() - .schemaString( - """ - type Query { - item: Item! - holder: Holder! - } - - type Item { - id: ID! - } - - type Holder { - value: Owner! - } - - type Owner { - name: String! + item { id value } + ownerItem { + id { name } + value { name } } - """) - .resolvers(GetterItemQueryResolver()) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - query { - item { id } - holder { value { name } } } """ } - assertEquals(data["item"], mapOf("id" to "2")) - assertEquals(data["holder"], mapOf("value" to mapOf("name" to "owner"))) + assertEquals(data["item"], mapOf("id" to "1", "value" to "1")) + assertEquals(data["ownerItem"], mapOf("id" to mapOf("name" to "owner"), "value" to mapOf("name" to "owner"))) } @Test - fun `type variables renamed by superclasses are resolved`() { + fun `type variables renamed and reordered by superclasses are resolved`() { val schema = SchemaParser.newParser() .schemaString( """ @@ -104,7 +69,7 @@ class InheritedTypeVariablesTest { name: String! } """) - .resolvers(AccountQueryResolver()) + .resolvers(QueryResolver()) .build() .makeExecutableSchema() val gql = GraphQL.newGraphQL(schema).build() @@ -120,48 +85,7 @@ class InheritedTypeVariablesTest { """ } - assertEquals(data["account"], mapOf("id" to "3", "owner" to mapOf("name" to "owner"))) - } - - @Test - fun `type variables reordered by a superclass are resolved`() { - val schema = SchemaParser.newParser() - .schemaString( - """ - type Query { - tuple: Tuple! - } - - type Tuple { - first: Owner! - second: Tag! - } - - type Owner { - name: String! - } - - type Tag { - label: String! - } - """) - .resolvers(TupleQueryResolver()) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - query { - tuple { - first { name } - second { label } - } - } - """ - } - - assertEquals(data["tuple"], mapOf("first" to mapOf("name" to "owner"), "second" to mapOf("label" to "tag"))) + assertEquals(data["account"], mapOf("id" to "2", "owner" to mapOf("name" to "owner"))) } @Test @@ -175,17 +99,24 @@ class InheritedTypeVariablesTest { type OwnerConnection { edges: [OwnerEdge!]! + nodes: [Owner]! + entries: [OwnerEntry!]! } type OwnerEdge { node: Owner! } + type OwnerEntry { + position: Int! + node: Owner! + } + type Owner { name: String! } """) - .resolvers(ConnectionQueryResolver()) + .resolvers(QueryResolver()) .build() .makeExecutableSchema() val gql = GraphQL.newGraphQL(schema).build() @@ -197,12 +128,22 @@ class InheritedTypeVariablesTest { edges { node { name } } + nodes { name } + entries { + position + node { name } + } } } """ } - assertEquals(data["owners"], mapOf("edges" to listOf(mapOf("node" to mapOf("name" to "owner"))))) + val owner = mapOf("name" to "owner") + assertEquals(data["owners"], mapOf( + "edges" to listOf(mapOf("node" to owner)), + "nodes" to listOf(owner), + "entries" to listOf(mapOf("position" to 0, "node" to owner)) + )) } @Test @@ -246,84 +187,6 @@ class InheritedTypeVariablesTest { assertEquals(data["tree"], mapOf("grouped" to mapOf("value" to listOf("leaf")), "async" to mapOf("value" to "leaf"))) } - @Test - fun `type variables in wildcard bounds are resolved`() { - val schema = SchemaParser.newParser() - .schemaString( - """ - type Query { - owners: OwnerList! - } - - type OwnerList { - items: [Owner]! - } - - type Owner { - name: String! - } - """) - .resolvers(WildcardQueryResolver()) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - query { - owners { - items { name } - } - } - """ - } - - assertEquals(data["owners"], mapOf("items" to listOf(mapOf("name" to "owner")))) - } - - @Test - fun `type variables of outer classes are resolved`() { - val schema = SchemaParser.newParser() - .schemaString( - """ - type Query { - owners: OwnerListing! - } - - type OwnerListing { - entries: [OwnerEntry!]! - } - - type OwnerEntry { - position: Int! - value: Owner! - } - - type Owner { - name: String! - } - """) - .resolvers(ListingQueryResolver()) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - query { - owners { - entries { - position - value { name } - } - } - } - """ - } - - assertEquals(data["owners"], mapOf("entries" to listOf(mapOf("position" to 0, "value" to mapOf("name" to "owner"))))) - } - @Test fun `generic types bound differently by subclasses can't share a type`() { val error = assertThrows(SchemaClassScannerError::class.java) { @@ -331,15 +194,15 @@ class InheritedTypeVariablesTest { .schemaString( """ type Query { - owners: OwnerPage! - tags: TagPage! + ownerPage: OwnerPage! + accountPage: AccountPage! } type OwnerPage { meta: Meta! } - type TagPage { + type AccountPage { meta: Meta! } @@ -347,7 +210,7 @@ class InheritedTypeVariablesTest { total: Int! } """) - .resolvers(MetaQueryResolver()) + .resolvers(QueryResolver()) .build() .makeExecutableSchema() } @@ -355,7 +218,7 @@ class InheritedTypeVariablesTest { val message = error.message.orEmpty() assert(message.startsWith("Two different classes used for type Meta")) { message } assert(message.contains("${Meta::class.java.name}<${Owner::class.java.name}>")) { message } - assert(message.contains("${Meta::class.java.name}<${Tag::class.java.name}>")) { message } + assert(message.contains("${Meta::class.java.name}<${Account::class.java.name}>")) { message } } @Test @@ -380,97 +243,46 @@ class InheritedTypeVariablesTest { assert(error.message.orEmpty().startsWith("Could not resolve type variable")) { error.message.orEmpty() } } - class FieldItemQueryResolver : GraphQLQueryResolver { - fun item(): FieldItem = FieldItem(1) - fun holder(): FieldHolder = FieldHolder(Owner("owner")) + class QueryResolver : GraphQLQueryResolver { + fun item(): Item = Item(1) + fun ownerItem(): BaseItem = BaseItem(Owner("owner")) + fun account(): Account = Account(2, Owner("owner")) + fun owners(): OwnerConnection = OwnerConnection(listOf(Owner("owner"))) + fun ownerPage(): OwnerPage = OwnerPage() + fun accountPage(): AccountPage = AccountPage() } - open class AbstractFieldItem(@JvmField val id: T) - - open class BaseFieldItem(id: T) : AbstractFieldItem(id) + // id is a public field, value a getter + open class AbstractItem(@JvmField val id: T, val value: T) - class FieldItem(id: Long) : BaseFieldItem(id) + open class BaseItem(value: T) : AbstractItem(value, value) - open class BaseFieldHolder(@JvmField val value: U) - - class FieldHolder(value: T) : BaseFieldHolder(value) - - class GetterItemQueryResolver : GraphQLQueryResolver { - fun item(): GetterItem = GetterItem(2) - fun holder(): OwnerHolder = OwnerHolder(Owner("owner")) - } - - open class AbstractGetterItem(val id: T) - - open class BaseGetterItem(id: T) : AbstractGetterItem(id) - - class GetterItem(id: Long) : BaseGetterItem(id) - - open class AbstractHolder(val value: T) - - open class BaseHolder(value: T) : AbstractHolder(value) - - class OwnerHolder(value: Owner) : BaseHolder(value) - - class AccountQueryResolver : GraphQLQueryResolver { - fun account(): Account = Account(3, Owner("owner")) - } + class Item(value: Long) : BaseItem(value) interface Identifiable { val id: I } - abstract class OwnedEntity(override val id: K, val owner: R) : Identifiable - - abstract class AuditableEntity(id: E, owner: O) : OwnedEntity(id, owner) - - class Account(id: Long, owner: Owner) : AuditableEntity(id, owner) - - class TupleQueryResolver : GraphQLQueryResolver { - fun tuple(): OwnerTagTuple = OwnerTagTuple(Owner("owner"), Tag("tag")) - } - - open class Tuple(val first: A, val second: B) - - open class ReversedTuple(first: B, second: A) : Tuple(first, second) - - class OwnerTagTuple(first: Owner, second: Tag) : ReversedTuple(first, second) - - class ConnectionQueryResolver : GraphQLQueryResolver { - fun owners(): OwnerConnection = OwnerConnection(listOf(Edge(Owner("owner")))) - } - - abstract class Connection(val edges: List>) - - class Edge(val node: T) - - class OwnerConnection(edges: List>) : Connection(edges) - - class WildcardQueryResolver : GraphQLQueryResolver { - fun owners(): OwnerList = OwnerList(listOf(Owner("owner"))) - } + abstract class Entity(override val id: K, val owner: R) : Identifiable - abstract class WildcardList(val items: List<@JvmWildcard T>) + // passes its K as Entity's R and vice versa + abstract class SwappedEntity(id: R, owner: K) : Entity(id, owner) - class OwnerList(items: List) : WildcardList(items) + class Account(id: Long, owner: Owner) : SwappedEntity(id, owner) - class ListingQueryResolver : GraphQLQueryResolver { - fun owners(): OwnerListing = OwnerListing(listOf(Owner("owner"))) - } + abstract class Connection(val nodes: List<@JvmWildcard T>) { + val edges: List> + get() = nodes.map { Edge(it) } - abstract class Listing(private val values: List) { val entries: List - get() = values.mapIndexed { position, value -> Entry(position, value) } + get() = nodes.mapIndexed { position, node -> Entry(position, node) } - inner class Entry(val position: Int, val value: T) + inner class Entry(val position: Int, val node: T) } - class OwnerListing(values: List) : Listing(values) + class Edge(val node: T) - class MetaQueryResolver : GraphQLQueryResolver { - fun owners(): OwnerPage = OwnerPage() - fun tags(): TagPage = TagPage() - } + class OwnerConnection(nodes: List) : Connection(nodes) class Meta(val total: Int) @@ -480,7 +292,7 @@ class InheritedTypeVariablesTest { class OwnerPage : MetaPage() - class TagPage : MetaPage() + class AccountPage : MetaPage() open class GenericMethodBase @@ -491,6 +303,4 @@ class InheritedTypeVariablesTest { } class Owner(val name: String) - - class Tag(val label: String) }