Skip to content

Commit 92468df

Browse files
authored
Added schema / introspection diff support (#745)
* Added schema / introspection diff support * Moved package to a more sensible one
1 parent 06ca6c3 commit 92468df

26 files changed

+2838
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package graphql.language;
2+
3+
import graphql.Assert;
4+
import graphql.PublicApi;
5+
6+
/**
7+
* And enumeration of the the kind of things that can be in a graphql type system
8+
*/
9+
@PublicApi
10+
public enum TypeKind {
11+
12+
Operation, Object, Interface, Union, Enum, Scalar, InputObject;
13+
14+
public static TypeKind getTypeKind(TypeDefinition def) {
15+
if (def instanceof ObjectTypeDefinition) {
16+
return Object;
17+
}
18+
if (def instanceof InterfaceTypeDefinition) {
19+
return Interface;
20+
}
21+
if (def instanceof UnionTypeDefinition) {
22+
return Union;
23+
}
24+
if (def instanceof ScalarTypeDefinition) {
25+
return Scalar;
26+
}
27+
if (def instanceof EnumTypeDefinition) {
28+
return Enum;
29+
}
30+
if (def instanceof InputObjectTypeDefinition) {
31+
return InputObject;
32+
}
33+
return Assert.assertShouldNeverHappen();
34+
}
35+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package graphql.schema.diff;
2+
3+
import graphql.PublicApi;
4+
5+
/**
6+
* A classification of difference events.
7+
*/
8+
@PublicApi
9+
public enum DiffCategory {
10+
/**
11+
* The new API is missing something compared to the old API
12+
*/
13+
MISSING,
14+
/**
15+
* The new API has become stricter for existing clients than the old API
16+
*/
17+
STRICTER,
18+
/**
19+
* The new API has an invalid structure
20+
*/
21+
INVALID,
22+
/**
23+
* The new API has added something not present in the old API
24+
*/
25+
ADDITION,
26+
/**
27+
* The new API has changed something compared to the old API
28+
*/
29+
DIFFERENT
30+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package graphql.schema.diff;
2+
3+
import graphql.Internal;
4+
import graphql.schema.diff.reporting.DifferenceReporter;
5+
import graphql.language.Document;
6+
import graphql.language.Type;
7+
import graphql.language.TypeDefinition;
8+
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
import java.util.Optional;
12+
import java.util.Stack;
13+
14+
/*
15+
* A helper class that represents diff state (eg visited types) as well as helpers
16+
*/
17+
@Internal
18+
class DiffCtx {
19+
final List<String> examinedTypes = new ArrayList<>();
20+
final Stack<String> currentTypes = new Stack<>();
21+
private final DifferenceReporter reporter;
22+
final Document oldDoc;
23+
final Document newDoc;
24+
25+
DiffCtx(DifferenceReporter reporter, Document oldDoc, Document newDoc) {
26+
this.reporter = reporter;
27+
this.oldDoc = oldDoc;
28+
this.newDoc = newDoc;
29+
}
30+
31+
void report(DiffEvent differenceEvent) {
32+
reporter.report(differenceEvent);
33+
}
34+
35+
boolean examiningType(String typeName) {
36+
if (examinedTypes.contains(typeName)) {
37+
return true;
38+
}
39+
examinedTypes.add(typeName);
40+
currentTypes.push(typeName);
41+
return false;
42+
}
43+
44+
void exitType() {
45+
currentTypes.pop();
46+
}
47+
48+
<T extends TypeDefinition> Optional<T> getOldTypeDef(Type type, Class<T> typeDefClass) {
49+
return getType(SchemaDiff.getTypeName(type), typeDefClass, oldDoc);
50+
}
51+
52+
<T extends TypeDefinition> Optional<T> getNewTypeDef(Type type, Class<T> typeDefClass) {
53+
return getType(SchemaDiff.getTypeName(type), typeDefClass, newDoc);
54+
}
55+
56+
private <T extends TypeDefinition> Optional<T> getType(String typeName, Class<T> typeDefClass, Document doc) {
57+
if (typeName == null) {
58+
return Optional.empty();
59+
}
60+
return doc.getDefinitions().stream()
61+
.filter(def -> typeDefClass.isAssignableFrom(def.getClass()))
62+
.map(typeDefClass::cast)
63+
.filter(defT -> defT.getName().equals(typeName))
64+
.findFirst();
65+
}
66+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package graphql.schema.diff;
2+
3+
import graphql.PublicApi;
4+
import graphql.language.TypeKind;
5+
6+
import java.util.ArrayList;
7+
import java.util.Arrays;
8+
import java.util.List;
9+
10+
import static java.util.stream.Collectors.toList;
11+
12+
/**
13+
* This represents the events that the {@link SchemaDiff} outputs.
14+
*/
15+
@PublicApi
16+
public class DiffEvent {
17+
18+
private final DiffLevel level;
19+
private final DiffCategory category;
20+
private final TypeKind typeOfType;
21+
private final String typeName;
22+
private final String fieldName;
23+
private final String reasonMsg;
24+
private final List<String> components;
25+
26+
DiffEvent(DiffLevel level, DiffCategory category, String typeName, String fieldName, TypeKind typeOfType, String reasonMsg, List<String> components) {
27+
this.level = level;
28+
this.category = category;
29+
this.typeName = typeName;
30+
this.fieldName = fieldName;
31+
this.typeOfType = typeOfType;
32+
this.reasonMsg = reasonMsg;
33+
this.components = components;
34+
}
35+
36+
public String getTypeName() {
37+
return typeName;
38+
}
39+
40+
public TypeKind getTypeKind() {
41+
return typeOfType;
42+
}
43+
44+
public String getReasonMsg() {
45+
return reasonMsg;
46+
}
47+
48+
public DiffLevel getLevel() {
49+
return level;
50+
}
51+
52+
public String getFieldName() {
53+
return fieldName;
54+
}
55+
56+
public DiffCategory getCategory() {
57+
return category;
58+
}
59+
60+
public List<String> getComponents() {
61+
return new ArrayList<>(components);
62+
}
63+
64+
@Override
65+
public String toString() {
66+
return "DifferenceEvent{" +
67+
" reasonMsg='" + reasonMsg + '\'' +
68+
", level=" + level +
69+
", category=" + category +
70+
", typeName='" + typeName + '\'' +
71+
", typeKind=" + typeOfType +
72+
", fieldName=" + fieldName +
73+
'}';
74+
}
75+
76+
public static Builder newInfo() {
77+
return new Builder().level(DiffLevel.INFO);
78+
}
79+
80+
public static Builder apiDanger() {
81+
return new Builder().level(DiffLevel.DANGEROUS);
82+
}
83+
84+
public static Builder apiBreakage() {
85+
return new Builder().level(DiffLevel.BREAKING);
86+
}
87+
88+
89+
public static class Builder {
90+
91+
DiffCategory category;
92+
DiffLevel level;
93+
String typeName;
94+
TypeKind typeOfType;
95+
String reasonMsg;
96+
String fieldName;
97+
List<String> components = new ArrayList<>();
98+
99+
public Builder level(DiffLevel level) {
100+
this.level = level;
101+
return this;
102+
}
103+
104+
105+
public Builder typeName(String typeName) {
106+
this.typeName = typeName;
107+
return this;
108+
}
109+
110+
public Builder fieldName(String fieldName) {
111+
this.fieldName = fieldName;
112+
return this;
113+
}
114+
115+
public Builder typeKind(TypeKind typeOfType) {
116+
this.typeOfType = typeOfType;
117+
return this;
118+
}
119+
120+
public Builder category(DiffCategory category) {
121+
this.category = category;
122+
return this;
123+
}
124+
125+
public Builder reasonMsg(String format, Object... args) {
126+
this.reasonMsg = String.format(format, args);
127+
return this;
128+
}
129+
130+
public Builder components(Object... args) {
131+
components.addAll(Arrays.stream(args).map(String::valueOf).collect(toList()));
132+
return this;
133+
}
134+
135+
public DiffEvent build() {
136+
return new DiffEvent(level, category, typeName, fieldName, typeOfType, reasonMsg, components);
137+
}
138+
}
139+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package graphql.schema.diff;
2+
3+
import graphql.PublicApi;
4+
5+
/**
6+
* This is the level of difference between graphql APIs
7+
*/
8+
@PublicApi
9+
public enum DiffLevel {
10+
/**
11+
* A simple info object coming out of the difference engine
12+
*/
13+
INFO,
14+
/**
15+
* The new API has made a breaking change
16+
*/
17+
BREAKING,
18+
/**
19+
* The new API has made a dangerous (but non breaking) change
20+
*/
21+
DANGEROUS
22+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package graphql.schema.diff;
2+
3+
import graphql.Assert;
4+
import graphql.ExecutionResult;
5+
import graphql.GraphQL;
6+
import graphql.PublicApi;
7+
import graphql.introspection.IntrospectionQuery;
8+
import graphql.schema.GraphQLSchema;
9+
10+
import java.util.Map;
11+
12+
/**
13+
* Represents 2 schemas that can be diffed. The {@link SchemaDiff} code
14+
* assumes that that schemas to be diffed are the result of a
15+
* {@link graphql.introspection.IntrospectionQuery}.
16+
*/
17+
@PublicApi
18+
public class DiffSet {
19+
20+
private final Map<String, Object> introspectionOld;
21+
private final Map<String, Object> introspectionNew;
22+
23+
public DiffSet(Map<String, Object> introspectionOld, Map<String, Object> introspectionNew) {
24+
this.introspectionOld = introspectionOld;
25+
this.introspectionNew = introspectionNew;
26+
}
27+
28+
/**
29+
* @return the old API as an introspection result
30+
*/
31+
public Map<String, Object> getOld() {
32+
return introspectionOld;
33+
}
34+
35+
/**
36+
* @return the new API as an introspection result
37+
*/
38+
public Map<String, Object> getNew() {
39+
return introspectionNew;
40+
}
41+
42+
43+
/**
44+
* Creates a diff set out of the result of 2 introspection queries.
45+
*
46+
* @param introspectionOld the older introspection query
47+
* @param introspectionNew the newer introspection query
48+
*
49+
* @return a diff set representing them
50+
*/
51+
public static DiffSet diffSet(Map<String, Object> introspectionOld, Map<String, Object> introspectionNew) {
52+
return new DiffSet(introspectionOld, introspectionNew);
53+
}
54+
55+
/**
56+
* Creates a diff set out of the result of 2 schemas.
57+
*
58+
* @param schemaOld the older schema
59+
* @param schemaNew the newer schema
60+
*
61+
* @return a diff set representing them
62+
*/
63+
public static DiffSet diffSet(GraphQLSchema schemaOld, GraphQLSchema schemaNew) {
64+
Map<String, Object> introspectionOld = introspect(schemaOld);
65+
Map<String, Object> introspectionNew = introspect(schemaNew);
66+
return diffSet(introspectionOld, introspectionNew);
67+
}
68+
69+
private static Map<String, Object> introspect(GraphQLSchema schema) {
70+
GraphQL gql = GraphQL.newGraphQL(schema).build();
71+
ExecutionResult result = gql.execute(IntrospectionQuery.INTROSPECTION_QUERY);
72+
Assert.assertTrue(result.getErrors().size() == 0, "The schema has errors during Introspection");
73+
return result.getData();
74+
}
75+
}

0 commit comments

Comments
 (0)