Skip to content

Commit 82e37dc

Browse files
committed
field validation (wip)
1 parent ec66d8b commit 82e37dc

4 files changed

Lines changed: 194 additions & 11 deletions

File tree

src/main/java/graphql/validation/ValidationErrorType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public enum ValidationErrorType {
2424
MisplacedDirective,
2525
UndefinedVariable,
2626
UnusedVariable,
27-
FragmentCycle
27+
FragmentCycle,
28+
FieldsConflict
2829

2930
}
Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,130 @@
11
package graphql.validation.rules;
22

33

4+
import graphql.execution.TypeFromAST;
5+
import graphql.language.*;
6+
import graphql.schema.GraphQLFieldDefinition;
7+
import graphql.schema.GraphQLFieldsContainer;
8+
import graphql.schema.GraphQLOutputType;
9+
import graphql.schema.GraphQLType;
410
import graphql.validation.AbstractRule;
511
import graphql.validation.ValidationContext;
612
import graphql.validation.ValidationErrorCollector;
713

8-
public class OverlappingFieldsCanBeMerged extends AbstractRule{
14+
import java.util.*;
15+
16+
public class OverlappingFieldsCanBeMerged extends AbstractRule {
17+
918

10-
// TODO
1119
public OverlappingFieldsCanBeMerged(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) {
1220
super(validationContext, validationErrorCollector);
1321
}
22+
23+
@Override
24+
public void checkSelectionSet(SelectionSet selectionSet) {
25+
Map<String, List<FieldAndType>> fieldMap = new LinkedHashMap<>();
26+
Set<String> visitedFragmentSpreads = new LinkedHashSet<>();
27+
collectFields(fieldMap, selectionSet, getValidationContext().getOutputType(), visitedFragmentSpreads);
28+
List<Conflict> conflicts = findConflicts(fieldMap);
29+
for (Conflict conflict : conflicts) {
30+
// addError(new ValidationError(ValidationErrorType.FieldsConflict));
31+
}
32+
33+
}
34+
35+
private List<Conflict> findConflicts(Map<String, List<FieldAndType>> fieldMap) {
36+
List<Conflict> result = new ArrayList<>();
37+
for (String name : fieldMap.keySet()) {
38+
List<FieldAndType> fieldAndTypes = fieldMap.get(name);
39+
for (int i = 0; i < fieldAndTypes.size(); i++) {
40+
for (int j = i + i; j < fieldAndTypes.size(); j++) {
41+
Conflict conflict = findConflict(name, fieldAndTypes.get(i), fieldAndTypes.get(j));
42+
if (conflict != null) {
43+
result.add(conflict);
44+
}
45+
}
46+
}
47+
}
48+
return result;
49+
}
50+
51+
private Conflict findConflict(String responseName, FieldAndType fieldAndType1, FieldAndType fieldAndType2) {
52+
53+
Field field1 = fieldAndType1.field;
54+
Field field2 = fieldAndType2.field;
55+
56+
String fieldName1 = field1.getName();
57+
String fieldName2 = field2.getName();
58+
if (!fieldName1.equals(fieldName2)) {
59+
String reason = String.format("%s and %s are different fields", fieldName1, fieldName2);
60+
return new Conflict(responseName, reason, field1, field2);
61+
}
62+
return null;
63+
64+
}
65+
66+
67+
private void collectFields(Map<String, List<FieldAndType>> fieldMap, SelectionSet selectionSet, GraphQLOutputType parentType, Set<String> visitedFragmentSpreads) {
68+
69+
for (Selection selection : selectionSet.getSelections()) {
70+
if (selection instanceof Field) {
71+
Field field = (Field) selection;
72+
String responseName = field.getAlias();
73+
if (!fieldMap.containsKey(responseName)) {
74+
fieldMap.put(responseName, new ArrayList<FieldAndType>());
75+
}
76+
GraphQLOutputType fieldType = null;
77+
if(parentType instanceof GraphQLFieldsContainer) {
78+
GraphQLFieldsContainer fieldsContainer = (GraphQLFieldsContainer) parentType;
79+
GraphQLFieldDefinition fieldDefinition = fieldsContainer.getFieldDefinition(((Field) selection).getName());
80+
fieldType = fieldDefinition != null ? fieldDefinition.getType() : null;
81+
}
82+
fieldMap.get(responseName).add(new FieldAndType(field, fieldType));
83+
84+
} else if (selection instanceof InlineFragment) {
85+
InlineFragment inlineFragment = (InlineFragment) selection;
86+
GraphQLOutputType graphQLType = (GraphQLOutputType) TypeFromAST.getTypeFromAST(getValidationContext().getSchema(),
87+
inlineFragment.getTypeCondition());
88+
collectFields(fieldMap, inlineFragment.getSelectionSet(), graphQLType, visitedFragmentSpreads);
89+
90+
} else if (selection instanceof FragmentSpread) {
91+
FragmentSpread fragmentSpread = (FragmentSpread) selection;
92+
FragmentDefinition fragment = getValidationContext().getFragment(fragmentSpread.getName());
93+
if (fragment == null) continue;
94+
if (visitedFragmentSpreads.contains(fragment.getName())) {
95+
continue;
96+
}
97+
visitedFragmentSpreads.add(fragment.getName());
98+
GraphQLOutputType graphQLType = (GraphQLOutputType) TypeFromAST.getTypeFromAST(getValidationContext().getSchema(),
99+
fragment.getTypeCondition());
100+
collectFields(fieldMap, fragment.getSelectionSet(), graphQLType, visitedFragmentSpreads);
101+
}
102+
}
103+
104+
}
105+
106+
private static class Conflict {
107+
String responseName;
108+
String reason;
109+
List<Field> fields = new ArrayList<>();
110+
111+
public Conflict(String responseName, String reason, Field field1, Field field2) {
112+
this.responseName = responseName;
113+
this.reason = reason;
114+
this.fields.add(field1);
115+
this.fields.add(field2);
116+
}
117+
118+
}
119+
120+
121+
private static class FieldAndType {
122+
public FieldAndType(Field field, GraphQLType graphQLType) {
123+
this.field = field;
124+
this.graphQLType = graphQLType;
125+
}
126+
127+
Field field;
128+
GraphQLType graphQLType;
129+
}
14130
}

src/test/groovy/graphql/UnionTest.groovy

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,13 @@ class UnionTest extends Specification {
106106
def "allows fragment conditions to be abstract types"() {
107107
given:
108108
def query = """
109-
{
110-
__typename
111-
name
112-
pets { ...PetFields }
113-
friends { ...FriendFields }
114-
}
115-
fragment PetFields on Pet {
109+
{
110+
__typename
111+
name
112+
pets { ...PetFields }
113+
friends { ...FriendFields }
114+
}
115+
fragment PetFields on Pet {
116116
__typename
117117
... on Dog {
118118
name
@@ -123,7 +123,7 @@ class UnionTest extends Specification {
123123
meows
124124
}
125125
}
126-
fragment FriendFields on Named {
126+
fragment FriendFields on Named {
127127
__typename
128128
name
129129
... on Dog {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package graphql.validation.rules
2+
3+
import graphql.Scalars
4+
import graphql.language.Document
5+
import graphql.parser.Parser
6+
import graphql.schema.GraphQLFieldDefinition
7+
import graphql.schema.GraphQLObjectType
8+
import graphql.schema.GraphQLSchema
9+
import graphql.validation.*
10+
import spock.lang.Ignore
11+
import spock.lang.Specification
12+
13+
class OverlappingFieldsCanBeMergedTest extends Specification {
14+
15+
ValidationErrorCollector errorCollector = new ValidationErrorCollector()
16+
17+
18+
def traverse(String query) {
19+
def objectType = GraphQLObjectType.newObject()
20+
.name("Test")
21+
.field(GraphQLFieldDefinition.newFieldDefinition().name("name").type(Scalars.GraphQLString).build())
22+
.field(GraphQLFieldDefinition.newFieldDefinition().name("nickname").type(Scalars.GraphQLString).build())
23+
.build();
24+
def schema = GraphQLSchema.newSchema().query(objectType).build()
25+
26+
Document document = new Parser().parseDocument(query)
27+
ValidationContext validationContext = new ValidationContext(schema, document)
28+
OverlappingFieldsCanBeMerged overlappingFieldsCanBeMerged = new OverlappingFieldsCanBeMerged(validationContext, errorCollector)
29+
LanguageTraversal languageTraversal = new LanguageTraversal();
30+
31+
languageTraversal.traverse(document, new RulesVisitor(validationContext, [overlappingFieldsCanBeMerged]));
32+
}
33+
34+
@Ignore
35+
def "identical fields are ok"() {
36+
given:
37+
def query = """
38+
fragment f on Test{
39+
name
40+
name
41+
}
42+
"""
43+
when:
44+
traverse(query)
45+
46+
then:
47+
errorCollector.errors.isEmpty()
48+
}
49+
50+
@Ignore
51+
def "two aliases with different targets"() {
52+
given:
53+
def query = """
54+
fragment f on Test{
55+
myName : name
56+
myName : nickname
57+
}
58+
"""
59+
when:
60+
traverse(query)
61+
62+
then:
63+
errorCollector.containsValidationError(ValidationErrorType.FieldsConflict)
64+
}
65+
66+
}

0 commit comments

Comments
 (0)