Skip to content

Commit a62601f

Browse files
authored
Merge pull request graphql-java#408 from bbakerman/406-extend-type-support-in-idl
graphql-java#406 added extend type XXX {} support
2 parents 14cb734 + 2afaad6 commit a62601f

14 files changed

+861
-35
lines changed

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

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

33
import graphql.GraphQLError;
4-
import graphql.language.*;
5-
import graphql.schema.*;
4+
import graphql.language.ArrayValue;
5+
import graphql.language.BooleanValue;
6+
import graphql.language.Comment;
7+
import graphql.language.EnumTypeDefinition;
8+
import graphql.language.EnumValue;
9+
import graphql.language.FieldDefinition;
10+
import graphql.language.FloatValue;
11+
import graphql.language.InputObjectTypeDefinition;
12+
import graphql.language.InputValueDefinition;
13+
import graphql.language.IntValue;
14+
import graphql.language.InterfaceTypeDefinition;
15+
import graphql.language.Node;
16+
import graphql.language.ObjectTypeDefinition;
17+
import graphql.language.ObjectValue;
18+
import graphql.language.OperationTypeDefinition;
19+
import graphql.language.ScalarTypeDefinition;
20+
import graphql.language.SchemaDefinition;
21+
import graphql.language.StringValue;
22+
import graphql.language.Type;
23+
import graphql.language.TypeDefinition;
24+
import graphql.language.TypeExtensionDefinition;
25+
import graphql.language.UnionTypeDefinition;
26+
import graphql.language.Value;
27+
import graphql.schema.DataFetcher;
28+
import graphql.schema.GraphQLArgument;
29+
import graphql.schema.GraphQLEnumType;
30+
import graphql.schema.GraphQLFieldDefinition;
31+
import graphql.schema.GraphQLInputObjectField;
32+
import graphql.schema.GraphQLInputObjectType;
33+
import graphql.schema.GraphQLInputType;
34+
import graphql.schema.GraphQLInterfaceType;
35+
import graphql.schema.GraphQLObjectType;
36+
import graphql.schema.GraphQLOutputType;
37+
import graphql.schema.GraphQLScalarType;
38+
import graphql.schema.GraphQLSchema;
39+
import graphql.schema.GraphQLTypeReference;
40+
import graphql.schema.GraphQLUnionType;
41+
import graphql.schema.PropertyDataFetcher;
42+
import graphql.schema.TypeResolver;
43+
import graphql.schema.TypeResolverProxy;
644
import graphql.schema.idl.errors.SchemaProblem;
745

8-
import java.util.*;
46+
import java.util.Collections;
47+
import java.util.HashMap;
48+
import java.util.LinkedHashMap;
49+
import java.util.List;
50+
import java.util.Map;
51+
import java.util.Optional;
52+
import java.util.Stack;
953

1054
/**
1155
* This can generate a working runtime schema from a compiled type registry and runtime wiring
@@ -215,13 +259,61 @@ private GraphQLObjectType buildObjectType(BuildContext buildCtx, ObjectTypeDefin
215259
builder.name(typeDefinition.getName());
216260
builder.description(buildDescription(typeDefinition));
217261

218-
typeDefinition.getFieldDefinitions().forEach(fieldDef ->
219-
builder.field(buildField(buildCtx, typeDefinition, fieldDef)));
262+
List<TypeExtensionDefinition> typeExtensions = getTypeExtensionsOf(typeDefinition, buildCtx);
263+
264+
buildObjectTypeFields(buildCtx, typeDefinition, builder, typeExtensions);
265+
266+
buildObjectTypeInterfaces(buildCtx, typeDefinition, builder, typeExtensions);
220267

221-
typeDefinition.getImplements().forEach(type -> builder.withInterface((GraphQLInterfaceType) buildOutputType(buildCtx, type)));
222268
return builder.build();
223269
}
224270

271+
private void buildObjectTypeFields(BuildContext buildCtx, ObjectTypeDefinition typeDefinition, GraphQLObjectType.Builder builder, List<TypeExtensionDefinition> typeExtensions) {
272+
Map<String, GraphQLFieldDefinition> fieldDefinitions = new LinkedHashMap<>();
273+
274+
typeDefinition.getFieldDefinitions().forEach(fieldDef -> {
275+
GraphQLFieldDefinition newFieldDefinition = buildField(buildCtx, typeDefinition, fieldDef);
276+
fieldDefinitions.put(newFieldDefinition.getName(), newFieldDefinition);
277+
});
278+
279+
// an object consists of the fields it gets from its definition AND its type extensions
280+
typeExtensions.forEach(typeExt -> typeExt.getFieldDefinitions().forEach(fieldDef -> {
281+
GraphQLFieldDefinition newFieldDefinition = buildField(buildCtx, typeDefinition, fieldDef);
282+
//
283+
// de-dupe here - pre-flight checks ensure all dupes are of the same type
284+
if (!fieldDefinitions.containsKey(newFieldDefinition.getName())) {
285+
fieldDefinitions.put(newFieldDefinition.getName(), newFieldDefinition);
286+
}
287+
}));
288+
289+
fieldDefinitions.values().forEach(builder::field);
290+
}
291+
292+
private void buildObjectTypeInterfaces(BuildContext buildCtx, ObjectTypeDefinition typeDefinition, GraphQLObjectType.Builder builder, List<TypeExtensionDefinition> typeExtensions) {
293+
Map<String, GraphQLInterfaceType> interfaces = new LinkedHashMap<>();
294+
typeDefinition.getImplements().forEach(type -> {
295+
GraphQLInterfaceType newInterfaceType = buildOutputType(buildCtx, type);
296+
interfaces.put(newInterfaceType.getName(), newInterfaceType);
297+
});
298+
299+
// an object consists of the interfaces it gets from its definition AND its type extensions
300+
typeExtensions.forEach(typeExt -> typeExt.getImplements().forEach(type -> {
301+
GraphQLInterfaceType interfaceType = buildOutputType(buildCtx, type);
302+
//
303+
// de-dupe here - pre-flight checks ensure all dupes are of the same type
304+
if (!interfaces.containsKey(interfaceType.getName())) {
305+
interfaces.put(interfaceType.getName(), interfaceType);
306+
}
307+
}));
308+
309+
interfaces.values().forEach(builder::withInterface);
310+
}
311+
312+
private List<TypeExtensionDefinition> getTypeExtensionsOf(ObjectTypeDefinition objectTypeDefinition, BuildContext buildCtx) {
313+
List<TypeExtensionDefinition> typeExtensionDefinitions = buildCtx.typeRegistry.typeExtensions().get(objectTypeDefinition.getName());
314+
return typeExtensionDefinitions == null ? Collections.emptyList() : typeExtensionDefinitions;
315+
}
316+
225317
private GraphQLInterfaceType buildInterfaceType(BuildContext buildCtx, InterfaceTypeDefinition typeDefinition) {
226318
GraphQLInterfaceType.Builder builder = GraphQLInterfaceType.newInterface();
227319
builder.name(typeDefinition.getName());

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

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

33
import graphql.GraphQLError;
4+
import graphql.language.AstPrinter;
45
import graphql.language.FieldDefinition;
56
import graphql.language.InputObjectTypeDefinition;
67
import graphql.language.InputValueDefinition;
@@ -13,20 +14,24 @@
1314
import graphql.language.TypeExtensionDefinition;
1415
import graphql.language.TypeName;
1516
import graphql.language.UnionTypeDefinition;
17+
import graphql.schema.idl.errors.MissingInterfaceTypeError;
1618
import graphql.schema.idl.errors.MissingScalarImplementationError;
1719
import graphql.schema.idl.errors.MissingTypeError;
1820
import graphql.schema.idl.errors.MissingTypeResolverError;
1921
import graphql.schema.idl.errors.OperationTypesMustBeObjects;
2022
import graphql.schema.idl.errors.QueryOperationMissingError;
2123
import graphql.schema.idl.errors.SchemaMissingError;
2224
import graphql.schema.idl.errors.SchemaProblem;
25+
import graphql.schema.idl.errors.TypeExtensionFieldRedefinitionError;
26+
import graphql.schema.idl.errors.TypeExtensionMissingBaseTypeError;
2327

2428
import java.util.ArrayList;
2529
import java.util.Collection;
2630
import java.util.List;
2731
import java.util.Map;
2832
import java.util.Optional;
2933
import java.util.function.Consumer;
34+
import java.util.function.Function;
3035
import java.util.stream.Collectors;
3136

3237
/**
@@ -39,13 +44,16 @@ public class SchemaTypeChecker {
3944
public List<GraphQLError> checkTypeRegistry(TypeDefinitionRegistry typeRegistry, RuntimeWiring wiring) throws SchemaProblem {
4045
List<GraphQLError> errors = new ArrayList<>();
4146
checkForMissingTypes(errors, typeRegistry);
47+
48+
checkTypeExtensionsHaveCorrespondingType(errors, typeRegistry);
49+
checkTypeExtensionsFieldRedefinition(errors, typeRegistry);
50+
4251
checkSchemaInvariants(errors, typeRegistry);
4352

4453
checkScalarImplementationsArePresent(errors, typeRegistry, wiring);
4554
checkTypeResolversArePresent(errors, typeRegistry, wiring);
4655

4756
return errors;
48-
4957
}
5058

5159
private void checkSchemaInvariants(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry) {
@@ -73,11 +81,11 @@ private void checkSchemaInvariants(List<GraphQLError> errors, TypeDefinitionRegi
7381

7482
private void checkForMissingTypes(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry) {
7583
// type extensions
76-
Collection<TypeExtensionDefinition> typeExtensions = typeRegistry.typeExtensions().values();
84+
List<TypeExtensionDefinition> typeExtensions = typeRegistry.typeExtensions().values().stream().flatMap(Collection::stream).collect(Collectors.toList());
7785
typeExtensions.forEach(typeExtension -> {
7886

7987
List<Type> implementsTypes = typeExtension.getImplements();
80-
implementsTypes.forEach(checkTypeExists("type extension", typeRegistry, errors, typeExtension));
88+
implementsTypes.forEach(checkInterfaceTypeExists(typeRegistry, errors, typeExtension));
8189

8290
checkFieldTypesPresent(typeRegistry, errors, typeExtension, typeExtension.getFieldDefinitions());
8391

@@ -91,7 +99,7 @@ private void checkForMissingTypes(List<GraphQLError> errors, TypeDefinitionRegis
9199
objectTypes.forEach(objectType -> {
92100

93101
List<Type> implementsTypes = objectType.getImplements();
94-
implementsTypes.forEach(checkTypeExists("object", typeRegistry, errors, objectType));
102+
implementsTypes.forEach(checkInterfaceTypeExists(typeRegistry, errors, objectType));
95103

96104
checkFieldTypesPresent(typeRegistry, errors, objectType, objectType.getFieldDefinitions());
97105

@@ -128,6 +136,7 @@ private void checkForMissingTypes(List<GraphQLError> errors, TypeDefinitionRegis
128136
});
129137
}
130138

139+
131140
private void checkScalarImplementationsArePresent(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry, RuntimeWiring wiring) {
132141
typeRegistry.scalars().keySet().forEach(scalarName -> {
133142
if (!wiring.getScalars().containsKey(scalarName)) {
@@ -175,6 +184,88 @@ private Consumer<Type> checkTypeExists(String typeOfType, TypeDefinitionRegistry
175184
};
176185
}
177186

187+
private Consumer<? super Type> checkInterfaceTypeExists(TypeDefinitionRegistry typeRegistry, List<GraphQLError> errors, TypeDefinition typeDefinition) {
188+
return t -> {
189+
TypeInfo typeInfo = TypeInfo.typeInfo(t);
190+
TypeName unwrapped = typeInfo.getTypeName();
191+
Optional<TypeDefinition> type = typeRegistry.getType(unwrapped);
192+
if (!type.isPresent()) {
193+
errors.add(new MissingInterfaceTypeError("interface", typeDefinition, unwrapped));
194+
} else if (!(type.get() instanceof InterfaceTypeDefinition)) {
195+
errors.add(new MissingInterfaceTypeError("interface", typeDefinition, unwrapped));
196+
}
197+
};
198+
}
199+
200+
/*
201+
A type can re-define a field if its actual the same type, but if they make 'fieldA : String' into
202+
'fieldA : Int' then we cant handle that. Even 'fieldA : String' to 'fieldA: String!' is tough to handle
203+
so we don't
204+
*/
205+
private void checkTypeExtensionsFieldRedefinition(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry) {
206+
Map<String, List<TypeExtensionDefinition>> typeExtensions = typeRegistry.typeExtensions();
207+
typeExtensions.values().forEach(extList -> extList.forEach(typeExtension -> {
208+
//
209+
// first check for field re-defs within a type ext
210+
for (TypeExtensionDefinition otherTypeExt : extList) {
211+
if (otherTypeExt == typeExtension) {
212+
continue;
213+
}
214+
// its the children that matter - the fields cannot be redefined
215+
checkForFieldRedefinition(errors, otherTypeExt, otherTypeExt.getFieldDefinitions(), typeExtension.getFieldDefinitions());
216+
}
217+
//
218+
// then check for field re-defs from the base type
219+
Optional<TypeDefinition> type = typeRegistry.getType(typeExtension.getName());
220+
if (type.isPresent() && type.get() instanceof ObjectTypeDefinition) {
221+
ObjectTypeDefinition baseType = (ObjectTypeDefinition) type.get();
222+
223+
checkForFieldRedefinition(errors, typeExtension, typeExtension.getFieldDefinitions(), baseType.getFieldDefinitions());
224+
}
225+
226+
}));
227+
228+
}
229+
230+
private void checkForFieldRedefinition(List<GraphQLError> errors, TypeDefinition typeDefinition, List<FieldDefinition> fieldDefinitions, List<FieldDefinition> referenceFieldDefinitions) {
231+
Map<String, FieldDefinition> referenceFields = referenceFieldDefinitions.stream()
232+
.collect(Collectors.toMap(
233+
FieldDefinition::getName, Function.identity()
234+
));
235+
236+
fieldDefinitions.forEach(fld -> {
237+
FieldDefinition referenceField = referenceFields.get(fld.getName());
238+
if (referenceFields.containsKey(fld.getName())) {
239+
// ok they have the same field but is it the same type
240+
if (!isSameType(fld.getType(), referenceField.getType())) {
241+
errors.add(new TypeExtensionFieldRedefinitionError(typeDefinition, fld));
242+
}
243+
}
244+
});
245+
}
246+
247+
private boolean isSameType(Type type1, Type type2) {
248+
String s1 = AstPrinter.printAst(type1);
249+
String s2 = AstPrinter.printAst(type2);
250+
return s1.equals(s2);
251+
}
252+
253+
private void checkTypeExtensionsHaveCorrespondingType(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry) {
254+
Map<String, List<TypeExtensionDefinition>> typeExtensions = typeRegistry.typeExtensions();
255+
typeExtensions.forEach((name, extTypeList) -> {
256+
TypeExtensionDefinition extensionDefinition = extTypeList.get(0);
257+
Optional<TypeDefinition> typeDefinition = typeRegistry.getType(new TypeName(name));
258+
if (!typeDefinition.isPresent()) {
259+
errors.add(new TypeExtensionMissingBaseTypeError(extensionDefinition));
260+
} else {
261+
if (!(typeDefinition.get() instanceof ObjectTypeDefinition)) {
262+
errors.add(new TypeExtensionMissingBaseTypeError(extensionDefinition));
263+
}
264+
}
265+
});
266+
}
267+
268+
178269
private Consumer<OperationTypeDefinition> checkOperationTypesExist(TypeDefinitionRegistry typeRegistry, List<GraphQLError> errors) {
179270
return op -> {
180271
TypeName unwrapped = TypeInfo.typeInfo(op.getType()).getTypeName();

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
public class TypeDefinitionRegistry {
2828

2929
private final Map<String, ScalarTypeDefinition> scalarTypes = new LinkedHashMap<>();
30-
private final Map<String, TypeExtensionDefinition> typeExtensions = new LinkedHashMap<>();
30+
private final Map<String, List<TypeExtensionDefinition>> typeExtensions = new LinkedHashMap<>();
3131
private final Map<String, TypeDefinition> types = new LinkedHashMap<>();
3232
private SchemaDefinition schema;
3333

@@ -49,9 +49,6 @@ public TypeDefinitionRegistry merge(TypeDefinitionRegistry typeRegistry) throws
4949
Map<String, ScalarTypeDefinition> tempScalarTypes = new LinkedHashMap<>();
5050
typeRegistry.scalarTypes.values().forEach(newEntry -> define(this.scalarTypes, tempScalarTypes, newEntry).ifPresent(errors::add));
5151

52-
Map<String, TypeExtensionDefinition> tempTypeExtensions = new LinkedHashMap<>();
53-
typeRegistry.typeExtensions.values().forEach(newEntry -> define(this.typeExtensions, tempTypeExtensions, newEntry).ifPresent(errors::add));
54-
5552
if (typeRegistry.schema != null && this.schema != null) {
5653
errors.add(new SchemaRedefinitionError(this.schema, typeRegistry.schema));
5754
}
@@ -63,8 +60,14 @@ public TypeDefinitionRegistry merge(TypeDefinitionRegistry typeRegistry) throws
6360
// ok commit to the merge
6461
this.schema = typeRegistry.schema;
6562
this.types.putAll(tempTypes);
66-
this.typeExtensions.putAll(tempTypeExtensions);
6763
this.scalarTypes.putAll(tempScalarTypes);
64+
//
65+
// merge type extensions since they can be redefined by design
66+
typeRegistry.typeExtensions.entrySet().forEach(newEntry -> {
67+
List<TypeExtensionDefinition> currentList = this.typeExtensions
68+
.computeIfAbsent(newEntry.getKey(), k -> new ArrayList<>());
69+
currentList.addAll(newEntry.getValue());
70+
});
6871

6972
return this;
7073
}
@@ -79,7 +82,7 @@ public TypeDefinitionRegistry merge(TypeDefinitionRegistry typeRegistry) throws
7982
public Optional<GraphQLError> add(Definition definition) {
8083
if (definition instanceof TypeExtensionDefinition) {
8184
TypeExtensionDefinition newEntry = (TypeExtensionDefinition) definition;
82-
return define(typeExtensions, typeExtensions, newEntry);
85+
return defineExt(typeExtensions, newEntry);
8386
} else if (definition instanceof ScalarTypeDefinition) {
8487
ScalarTypeDefinition newEntry = (ScalarTypeDefinition) definition;
8588
return define(scalarTypes, scalarTypes, newEntry);
@@ -109,6 +112,11 @@ private <T extends TypeDefinition> Optional<GraphQLError> define(Map<String, T>
109112
return Optional.empty();
110113
}
111114

115+
private Optional<GraphQLError> defineExt(Map<String, List<TypeExtensionDefinition>> typeExtensions, TypeExtensionDefinition newEntry) {
116+
List<TypeExtensionDefinition> currentList = typeExtensions.computeIfAbsent(newEntry.getName(), k -> new ArrayList<>());
117+
currentList.add(newEntry);
118+
return Optional.empty();
119+
}
112120

113121
public Map<String, TypeDefinition> types() {
114122
return new LinkedHashMap<>(types);
@@ -120,7 +128,7 @@ public Map<String, ScalarTypeDefinition> scalars() {
120128
return scalars;
121129
}
122130

123-
public Map<String, TypeExtensionDefinition> typeExtensions() {
131+
public Map<String, List<TypeExtensionDefinition>> typeExtensions() {
124132
return new LinkedHashMap<>(typeExtensions);
125133
}
126134

@@ -139,6 +147,10 @@ public boolean hasType(TypeName typeName) {
139147

140148
public Optional<TypeDefinition> getType(Type type) {
141149
String typeName = TypeInfo.typeInfo(type).getName();
150+
return getType(typeName);
151+
}
152+
153+
public Optional<TypeDefinition> getType(String typeName) {
142154
TypeDefinition typeDefinition = types.get(typeName);
143155
if (typeDefinition != null) {
144156
return Optional.of(typeDefinition);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package graphql.schema.idl.errors;
2+
3+
import graphql.language.TypeDefinition;
4+
import graphql.language.TypeName;
5+
6+
import static java.lang.String.format;
7+
8+
public class MissingInterfaceTypeError extends BaseError {
9+
10+
public MissingInterfaceTypeError(String typeOfType, TypeDefinition typeDefinition, TypeName typeName) {
11+
super(typeDefinition, format("The %s type '%s' is not present when resolving type '%s' %s",
12+
typeOfType, typeName.getName(), typeDefinition.getName(), lineCol(typeDefinition)));
13+
}
14+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
public class SchemaMissingError extends BaseError {
44

55
public SchemaMissingError() {
6-
super(null, "There is no ttop level schema object defined");
6+
super(null, "There is no top level schema object defined");
77
}
88
}

0 commit comments

Comments
 (0)