diff --git a/src/main/java/graphql/schema/idl/TypeDefinitionRegistry.java b/src/main/java/graphql/schema/idl/TypeDefinitionRegistry.java index 37bd404e4e..996bcb5c2b 100644 --- a/src/main/java/graphql/schema/idl/TypeDefinitionRegistry.java +++ b/src/main/java/graphql/schema/idl/TypeDefinitionRegistry.java @@ -37,9 +37,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.function.Function; +import java.util.stream.Stream; import static graphql.Assert.assertNotNull; import static graphql.schema.idl.SchemaExtensionsChecker.defineOperationDefs; @@ -719,18 +719,7 @@ public Map getTypesMap(Class targetClas public List getAllImplementationsOf(InterfaceTypeDefinition targetInterface) { return ImmutableKit.filter( getTypes(ImplementingTypeDefinition.class), - implementingTypeDefinition -> { - List> implementsList = implementingTypeDefinition.getImplements(); - for (Type iFace : implementsList) { - InterfaceTypeDefinition interfaceTypeDef = getTypeOrNull(iFace, InterfaceTypeDefinition.class); - if (interfaceTypeDef != null) { - if (interfaceTypeDef.getName().equals(targetInterface.getName())) { - return true; - } - } - } - return false; - }); + implementingTypeDefinition -> implementsInterface(implementingTypeDefinition, targetInterface)); } /** @@ -765,37 +754,42 @@ public boolean isPossibleType(Type abstractType, Type possibleType) { if (!isObjectTypeOrInterface(possibleType)) { return false; } - TypeDefinition targetObjectTypeDef = Objects.requireNonNull(getTypeOrNull(possibleType)); - TypeDefinition abstractTypeDef = Objects.requireNonNull(getTypeOrNull(abstractType)); + TypeDefinition possibleTypeDef = assertNotNull(getTypeOrNull(possibleType)); + TypeDefinition abstractTypeDef = assertNotNull(getTypeOrNull(abstractType)); if (abstractTypeDef instanceof UnionTypeDefinition) { - List memberTypes = ((UnionTypeDefinition) abstractTypeDef).getMemberTypes(); - for (Type memberType : memberTypes) { - ObjectTypeDefinition checkType = getTypeOrNull(memberType, ObjectTypeDefinition.class); - if (checkType != null) { - if (checkType.getName().equals(targetObjectTypeDef.getName())) { - return true; - } - } - } - return false; - } else { - InterfaceTypeDefinition iFace = (InterfaceTypeDefinition) abstractTypeDef; - for (TypeDefinition t : types.values()) { - if (t instanceof ImplementingTypeDefinition) { - if (t.getName().equals(targetObjectTypeDef.getName())) { - ImplementingTypeDefinition itd = (ImplementingTypeDefinition) t; - - for (Type implementsType : itd.getImplements()) { - TypeDefinition matchingInterface = types.get(typeName(implementsType)); - if (matchingInterface != null && matchingInterface.getName().equals(iFace.getName())) { - return true; - } - } - } - } - } - return false; + return isUnionMember((UnionTypeDefinition) abstractTypeDef, possibleTypeDef); + } + return implementsInterface( + (ImplementingTypeDefinition) possibleTypeDef, + (InterfaceTypeDefinition) abstractTypeDef); + } + + private boolean implementsInterface( + ImplementingTypeDefinition implementingType, + InterfaceTypeDefinition targetInterface) { + return Stream.concat( + Stream.of(implementingType), + getImplementingTypeExtensions(implementingType).stream()) + .flatMap(type -> type.getImplements().stream()) + .map(TypeInfo::typeName) + .anyMatch(targetInterface.getName()::equals); + } + + private List> getImplementingTypeExtensions( + ImplementingTypeDefinition implementingType) { + if (implementingType instanceof InterfaceTypeDefinition) { + return interfaceTypeExtensions.getOrDefault(implementingType.getName(), List.of()); } + return objectTypeExtensions.getOrDefault(implementingType.getName(), List.of()); + } + + private boolean isUnionMember(UnionTypeDefinition unionType, TypeDefinition possibleType) { + return Stream.concat( + unionType.getMemberTypes().stream(), + unionTypeExtensions.getOrDefault(unionType.getName(), List.of()).stream() + .flatMap(extension -> extension.getMemberTypes().stream())) + .map(TypeInfo::typeName) + .anyMatch(possibleType.getName()::equals); } /** diff --git a/src/test/groovy/graphql/schema/idl/SchemaTypeCheckerTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaTypeCheckerTest.groovy index a109e0583c..e72e43eb4b 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaTypeCheckerTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaTypeCheckerTest.groovy @@ -1337,6 +1337,171 @@ class SchemaTypeCheckerTest extends Specification { } + def "covariant object type implemented through an extension is supported"() { + def spec = ''' + type Query { + base: Base + } + + interface Pet { + id: ID + } + + type Dog { + id: ID + } + + extend type Dog implements Pet + + type Base { + foo: String + } + + interface PetContainer { + pet: Pet + } + + extend type Base implements PetContainer { + pet: Dog + } + ''' + + def result = check(spec, ["Pet", "PetContainer"]) + + expect: + result.isEmpty() + } + + def "covariant interface type implemented through an extension is supported"() { + def spec = ''' + type Query { + base: Base + } + + interface Pet { + id: ID + } + + interface WorkingPet { + id: ID + } + + extend interface WorkingPet implements Pet + + interface PetContainer { + pet: Pet + } + + type Base implements PetContainer { + pet: WorkingPet + } + ''' + + def result = check(spec, ["Pet", "WorkingPet", "PetContainer"]) + + expect: + result.isEmpty() + } + + def "covariant union member added through an extension is supported"() { + def spec = ''' + type Query { + base: Base + } + + type Cat { + id: ID + } + + type Dog { + id: ID + } + + union Pets = Cat + + extend union Pets = Dog + + interface PetContainer { + pet: Pets + } + + type Base implements PetContainer { + pet: Dog + } + ''' + + def result = check(spec, ["Pets", "PetContainer"]) + + expect: + result.isEmpty() + } + + def "wrapped covariant object type implemented through an extension is supported"() { + def spec = ''' + type Query { + base: Base + } + + interface Pet { + id: ID + } + + type Dog { + id: ID + } + + extend type Dog implements Pet + + interface PetContainer { + pets: [Pet]! + } + + type Base implements PetContainer { + pets: [Dog!]! + } + ''' + + def result = check(spec, ["Pet", "PetContainer"]) + + expect: + result.isEmpty() + } + + def "unrelated type remains invalid when other interface relationships use extensions"() { + def spec = ''' + type Query { + base: Base + } + + interface Pet { + id: ID + } + + interface Vehicle { + id: ID + } + + type Car { + id: ID + } + + extend type Car implements Vehicle + + interface PetContainer { + pet: Pet + } + + type Base implements PetContainer { + pet: Car + } + ''' + + def result = check(spec, ["Pet", "Vehicle", "PetContainer"]) + + expect: + errorContaining(result, "has tried to redefine field 'pet' defined via interface 'PetContainer'") + } + def "deviant covariant object types are detected"() { def spec = ''' diff --git a/src/test/groovy/graphql/schema/idl/TypeDefinitionRegistryTest.groovy b/src/test/groovy/graphql/schema/idl/TypeDefinitionRegistryTest.groovy index 0ebd6dc882..6ec0d9d85a 100644 --- a/src/test/groovy/graphql/schema/idl/TypeDefinitionRegistryTest.groovy +++ b/src/test/groovy/graphql/schema/idl/TypeDefinitionRegistryTest.groovy @@ -469,6 +469,87 @@ class TypeDefinitionRegistryTest extends Specification { ''' + def extensionRelationships = ''' + interface Pet { + id: ID + } + + interface WorkingPet { + id: ID + } + + extend interface WorkingPet implements Pet + + type Dog { + id: ID + } + + extend type Dog implements Pet + + type Cat { + id: ID + } + + union Pets = Cat + + extend union Pets = Dog + ''' + + def "possible type detection includes interface implementations from extensions for #typeOfReg registry"() { + when: + def registry = registry(extensionRelationships, typeOfReg) + + then: + registry.isPossibleType(type("Pet"), type("Dog")) + registry.isPossibleType(type("Pet"), type("WorkingPet")) + !registry.isPossibleType(type("Pet"), type("Cat")) + + where: + typeOfReg << ["mutable", "immutable"] + } + + def "possible type detection includes union members from extensions for #typeOfReg registry"() { + when: + def registry = registry(extensionRelationships, typeOfReg) + + then: + registry.isPossibleType(type("Pets"), type("Cat")) + registry.isPossibleType(type("Pets"), type("Dog")) + !registry.isPossibleType(type("Pets"), type("WorkingPet")) + + where: + typeOfReg << ["mutable", "immutable"] + } + + def "subtype detection unwraps types implemented through extensions for #typeOfReg registry"() { + when: + def registry = registry(extensionRelationships, typeOfReg) + + then: + registry.isSubTypeOf(nonNullType("Dog"), type("Pet")) + registry.isSubTypeOf(listType(type("Dog")), listType(type("Pet"))) + registry.isSubTypeOf( + listType(nonNullType(listType(type("Dog")))), + listType(nonNullType(listType(type("Pet"))))) + !registry.isSubTypeOf(type("Cat"), type("Pet")) + + where: + typeOfReg << ["mutable", "immutable"] + } + + def "implementation lookup includes relationships from extensions for #typeOfReg registry"() { + when: + def registry = registry(extensionRelationships, typeOfReg) + def pet = registry.getTypeOrNull("Pet", InterfaceTypeDefinition.class) + + then: + registry.getAllImplementationsOf(pet)*.name == ["WorkingPet", "Dog"] + registry.getImplementationsOf(pet)*.name == ["Dog"] + + where: + typeOfReg << ["mutable", "immutable"] + } + def "test possible type detection #typeOfReg"() { given: TypeDefinitionRegistry mutableReg = parse(animalia) @@ -506,6 +587,14 @@ class TypeDefinitionRegistryTest extends Specification { "immutable" | _ } + private static TypeDefinitionRegistry registry(String spec, String typeOfRegistry) { + def registry = parse(spec) + if (typeOfRegistry == "immutable") { + return registry.readOnly() + } + return registry + } + def "isSubTypeOf detection #typeOfReg"() { when: