Skip to content

Commit b4816f0

Browse files
committed
Added covariant support to SDL object definitions
1 parent 40d04ca commit b4816f0

7 files changed

Lines changed: 492 additions & 18 deletions

File tree

src/main/java/graphql/schema/idl/SchemaTypeChecker.java

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -441,9 +441,9 @@ private Consumer<? super Type> checkInterfaceIsImplemented(String typeOfType, Ty
441441
if (objectFieldDef == null) {
442442
errors.add(new MissingInterfaceFieldError(typeOfType, objectTypeDef, interfaceTypeDef, interfaceFieldDef));
443443
} else {
444-
String interfaceFieldType = AstPrinter.printAst(interfaceFieldDef.getType());
445-
String objectFieldType = AstPrinter.printAst(objectFieldDef.getType());
446-
if (!interfaceFieldType.equals(objectFieldType)) {
444+
if (!isTypeSubTypeOf(typeRegistry, objectFieldDef.getType(), interfaceFieldDef.getType())) {
445+
String interfaceFieldType = AstPrinter.printAst(interfaceFieldDef.getType());
446+
String objectFieldType = AstPrinter.printAst(objectFieldDef.getType());
447447
errors.add(new InterfaceFieldRedefinitionError(typeOfType, objectTypeDef, interfaceTypeDef, objectFieldDef, objectFieldType, interfaceFieldType));
448448
}
449449

@@ -461,6 +461,52 @@ private Consumer<? super Type> checkInterfaceIsImplemented(String typeOfType, Ty
461461
};
462462
}
463463

464+
@SuppressWarnings("SimplifiableIfStatement")
465+
private boolean isTypeSubTypeOf(TypeDefinitionRegistry registry, Type maybeSubType, Type superType) {
466+
TypeInfo maybeSubTypeInfo = TypeInfo.typeInfo(maybeSubType);
467+
TypeInfo superTypeInfo = TypeInfo.typeInfo(superType);
468+
// Equivalent type is a valid subtype
469+
if (maybeSubTypeInfo.equals(superTypeInfo)) {
470+
return true;
471+
}
472+
473+
474+
// If superType is non-null, maybeSubType must also be non-null.
475+
if (superTypeInfo.isNonNull()) {
476+
if (maybeSubTypeInfo.isNonNull()) {
477+
return isTypeSubTypeOf(registry, maybeSubTypeInfo.unwrapOneType(), superTypeInfo.unwrapOneType());
478+
}
479+
return false;
480+
}
481+
if (maybeSubTypeInfo.isNonNull()) {
482+
// If superType is nullable, maybeSubType may be non-null or nullable.
483+
return isTypeSubTypeOf(registry, maybeSubTypeInfo.unwrapOneType(), superType);
484+
}
485+
486+
// If superType type is a list, maybeSubType type must also be a list.
487+
if (superTypeInfo.isList()) {
488+
if (maybeSubTypeInfo.isList()) {
489+
return isTypeSubTypeOf(registry, maybeSubTypeInfo.unwrapOneType(), superTypeInfo.unwrapOneType());
490+
}
491+
return false;
492+
}
493+
if (maybeSubTypeInfo.isList()) {
494+
// If superType is not a list, maybeSubType must also be not a list.
495+
return false;
496+
}
497+
498+
// If superType type is an abstract type, maybeSubType type may be a currently
499+
// possible object type.
500+
if (registry.isAbstractType(superType) &&
501+
registry.isObjectType(maybeSubType) &&
502+
registry.isPossibleType(superType, maybeSubType)) {
503+
return true;
504+
}
505+
506+
// Otherwise, the child type is not a valid subtype of the parent type.
507+
return false;
508+
}
509+
464510
private void checkArgumentConsistency(String typeOfType, ObjectTypeDefinition objectTypeDef, InterfaceTypeDefinition interfaceTypeDef, FieldDefinition objectFieldDef, FieldDefinition interfaceFieldDef, List<GraphQLError> errors) {
465511
List<InputValueDefinition> objectArgs = objectFieldDef.getInputValueDefinitions();
466512
List<InputValueDefinition> interfaceArgs = interfaceFieldDef.getInputValueDefinitions();

src/main/java/graphql/schema/idl/TypeDefinitionRegistry.java

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,30 @@
55
import graphql.language.Definition;
66
import graphql.language.EnumTypeExtensionDefinition;
77
import graphql.language.InputObjectTypeExtensionDefinition;
8+
import graphql.language.InterfaceTypeDefinition;
89
import graphql.language.InterfaceTypeExtensionDefinition;
10+
import graphql.language.ObjectTypeDefinition;
911
import graphql.language.ObjectTypeExtensionDefinition;
1012
import graphql.language.ScalarTypeDefinition;
1113
import graphql.language.ScalarTypeExtensionDefinition;
1214
import graphql.language.SchemaDefinition;
1315
import graphql.language.Type;
1416
import graphql.language.TypeDefinition;
1517
import graphql.language.TypeName;
18+
import graphql.language.UnionTypeDefinition;
1619
import graphql.language.UnionTypeExtensionDefinition;
1720
import graphql.schema.idl.errors.SchemaProblem;
1821
import graphql.schema.idl.errors.SchemaRedefinitionError;
1922
import graphql.schema.idl.errors.TypeRedefinitionError;
23+
import graphql.util.FpKit;
2024

2125
import java.util.ArrayList;
2226
import java.util.LinkedHashMap;
2327
import java.util.List;
2428
import java.util.Map;
2529
import java.util.Optional;
2630
import java.util.function.Function;
31+
import java.util.stream.Collectors;
2732

2833
import static java.util.Optional.ofNullable;
2934

@@ -261,4 +266,117 @@ public <T extends TypeDefinition> Optional<T> getType(String typeName, Class<T>
261266
}
262267
return Optional.empty();
263268
}
269+
270+
/**
271+
* Returns true if the specified type exists in the registry and is an abstract (Interface or Union) type
272+
*
273+
* @param type the type to check
274+
*
275+
* @return true if its abstract
276+
*/
277+
public boolean isAbstractType(Type type) {
278+
Optional<TypeDefinition> typeDefinition = getType(type);
279+
if (typeDefinition.isPresent()) {
280+
TypeDefinition definition = typeDefinition.get();
281+
return definition instanceof UnionTypeDefinition || definition instanceof InterfaceTypeDefinition;
282+
}
283+
return false;
284+
}
285+
286+
/**
287+
* Returns true if the specified type exists in the registry and is an object type
288+
*
289+
* @param type the type to check
290+
*
291+
* @return true if its an object type
292+
*/
293+
public boolean isObjectType(Type type) {
294+
return getType(type, ObjectTypeDefinition.class).isPresent();
295+
}
296+
297+
/**
298+
* Returns a list of types in the registry of that specified class
299+
*
300+
* @param targetClass the class to search for
301+
* @param <T> must extend TypeDefinition
302+
*
303+
* @return a list of types of the target class
304+
*/
305+
public <T extends TypeDefinition> List<T> getTypes(Class<T> targetClass) {
306+
return types.values().stream()
307+
.filter(targetClass::isInstance)
308+
.map(targetClass::cast)
309+
.collect(Collectors.toList());
310+
}
311+
312+
/**
313+
* Returns a map of types in the registry of that specified class keyed by name
314+
*
315+
* @param targetClass the class to search for
316+
* @param <T> must extend TypeDefinition
317+
*
318+
* @return a map of types
319+
*/
320+
public <T extends TypeDefinition> Map<String, T> getTypesMap(Class<T> targetClass) {
321+
List<T> list = getTypes(targetClass);
322+
return FpKit.getByName(list, TypeDefinition::getName, FpKit.mergeFirst());
323+
}
324+
325+
/**
326+
* Returns the list of object types that implement the given interface type
327+
*
328+
* @param targetInterface the target to search for
329+
*
330+
* @return the list of object types that implement the given interface type
331+
*/
332+
public List<ObjectTypeDefinition> getImplementationsOf(InterfaceTypeDefinition targetInterface) {
333+
List<ObjectTypeDefinition> objectTypeDefinitions = getTypes(ObjectTypeDefinition.class);
334+
return objectTypeDefinitions.stream().filter(objectTypeDefinition -> {
335+
List<Type> implementsList = objectTypeDefinition.getImplements();
336+
for (Type iFace : implementsList) {
337+
Optional<InterfaceTypeDefinition> interfaceTypeDef = getType(iFace, InterfaceTypeDefinition.class);
338+
if (interfaceTypeDef.isPresent()) {
339+
boolean equals = interfaceTypeDef.get().getName().equals(targetInterface.getName());
340+
if (equals) {
341+
return true;
342+
}
343+
}
344+
}
345+
return false;
346+
}).collect(Collectors.toList());
347+
}
348+
349+
/**
350+
* Returns true of the abstract type is in implemented by the object type
351+
*
352+
* @param abstractType the abstract type to check (interface or union)
353+
* @param possibleObjectType the object type to check
354+
*
355+
* @return true if the object type implements the abstract type
356+
*/
357+
@SuppressWarnings("ConstantConditions")
358+
public boolean isPossibleType(Type abstractType, Type possibleObjectType) {
359+
if (!isAbstractType(abstractType)) {
360+
return false;
361+
}
362+
if (!isObjectType(possibleObjectType)) {
363+
return false;
364+
}
365+
ObjectTypeDefinition targetObjectTypeDef = getType(possibleObjectType, ObjectTypeDefinition.class).get();
366+
TypeDefinition abstractTypeDef = getType(abstractType).get();
367+
if (abstractTypeDef instanceof UnionTypeDefinition) {
368+
List<Type> memberTypes = ((UnionTypeDefinition) abstractTypeDef).getMemberTypes();
369+
for (Type memberType : memberTypes) {
370+
if (getType(memberType, ObjectTypeDefinition.class).isPresent()) {
371+
return true;
372+
}
373+
}
374+
return false;
375+
} else {
376+
InterfaceTypeDefinition iFace = (InterfaceTypeDefinition) abstractTypeDef;
377+
List<ObjectTypeDefinition> objectTypeDefinitions = getImplementationsOf(iFace);
378+
return objectTypeDefinitions.stream()
379+
.anyMatch(od -> od.getName().equals(targetObjectTypeDef.getName()));
380+
}
381+
}
264382
}

src/main/java/graphql/schema/idl/TypeInfo.java

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package graphql.schema.idl;
22

33
import graphql.Internal;
4+
import graphql.language.AstPrinter;
45
import graphql.language.ListType;
56
import graphql.language.NonNullType;
67
import graphql.language.Type;
@@ -9,6 +10,7 @@
910
import graphql.schema.GraphQLNonNull;
1011
import graphql.schema.GraphQLType;
1112

13+
import java.util.Objects;
1214
import java.util.Stack;
1315

1416
/**
@@ -25,7 +27,7 @@ public static TypeInfo typeInfo(Type type) {
2527
private final TypeName typeName;
2628
private final Stack<Class<?>> decoration = new Stack<>();
2729

28-
public TypeInfo(Type type) {
30+
private TypeInfo(Type type) {
2931
this.rawType = type;
3032
while (!(type instanceof TypeName)) {
3133
if (type instanceof NonNullType) {
@@ -93,13 +95,7 @@ public <T extends GraphQLType> T decorate(GraphQLType objectType) {
9395
}
9496

9597
public static String getAstDesc(Type type) {
96-
if (type instanceof NonNullType) {
97-
return getAstDesc(((NonNullType) type).getType()) + "!";
98-
}
99-
if (type instanceof ListType) {
100-
return "[" + getAstDesc(((ListType) type).getType()) + "]";
101-
}
102-
return ((TypeName) type).getName();
98+
return AstPrinter.printAst(type);
10399
}
104100

105101
public TypeInfo unwrapOne() {
@@ -112,12 +108,30 @@ public TypeInfo unwrapOne() {
112108
return this;
113109
}
114110

111+
public Type unwrapOneType() {
112+
return unwrapOne().getRawType();
113+
}
114+
115+
@Override
116+
public boolean equals(Object o) {
117+
if (this == o) return true;
118+
if (o == null || getClass() != o.getClass()) return false;
119+
TypeInfo typeInfo = (TypeInfo) o;
120+
return isNonNull() == typeInfo.isNonNull() &&
121+
isList() == typeInfo.isList() &&
122+
Objects.equals(typeName.getName(), typeInfo.typeName.getName());
123+
}
124+
125+
@Override
126+
public int hashCode() {
127+
return Objects.hash(typeName.getName(), isNonNull(), isList());
128+
}
129+
130+
115131
@Override
116132
public String toString() {
117133
return "TypeInfo{" +
118-
"rawType=" + rawType +
119-
", typeName=" + typeName +
120-
", isNonNull=" + decoration +
134+
getAstDesc(rawType) +
121135
'}';
122136
}
123137
}

src/main/java/graphql/schema/idl/errors/InterfaceFieldRedefinitionError.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
public class InterfaceFieldRedefinitionError extends BaseError {
1010
public InterfaceFieldRedefinitionError(String typeOfType, ObjectTypeDefinition objectType, InterfaceTypeDefinition interfaceTypeDef, FieldDefinition objectFieldDef, String objectFieldType, String interfaceFieldType) {
11-
super(objectType, format("The %s type '%s' %s has tried to redefine field '%s' defined via interface '%s' %s from '%s' to '%s",
11+
super(objectType, format("The %s type '%s' %s has tried to redefine field '%s' defined via interface '%s' %s from '%s' to '%s'",
1212
typeOfType, objectType.getName(), lineCol(objectType), objectFieldDef.getName(), interfaceTypeDef.getName(), lineCol(interfaceTypeDef), interfaceFieldType, objectFieldType));
1313
}
1414
}

0 commit comments

Comments
 (0)