Skip to content

Commit defeda1

Browse files
Copilotandimarek
andcommitted
Add OneOf inhabitability validation
Co-authored-by: andimarek <1706744+andimarek@users.noreply.github.com>
1 parent c7eea39 commit defeda1

3 files changed

Lines changed: 284 additions & 0 deletions

File tree

src/main/java/graphql/schema/validation/OneOfInputObjectRules.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44
import graphql.schema.GraphQLInputObjectField;
55
import graphql.schema.GraphQLInputObjectType;
66
import graphql.schema.GraphQLSchemaElement;
7+
import graphql.schema.GraphQLType;
78
import graphql.schema.GraphQLTypeUtil;
89
import graphql.schema.GraphQLTypeVisitorStub;
10+
import graphql.schema.GraphQLUnmodifiedType;
911
import graphql.util.TraversalControl;
1012
import graphql.util.TraverserContext;
1113

14+
import java.util.LinkedHashSet;
15+
import java.util.Set;
16+
1217
import static java.lang.String.format;
1318

1419
/*
@@ -19,6 +24,45 @@
1924
@ExperimentalApi
2025
public class OneOfInputObjectRules extends GraphQLTypeVisitorStub {
2126

27+
@Override
28+
public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType inputObjectType, TraverserContext<GraphQLSchemaElement> context) {
29+
if (!inputObjectType.isOneOf()) {
30+
return TraversalControl.CONTINUE;
31+
}
32+
SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class);
33+
if (!canBeProvidedAFiniteValue(inputObjectType, new LinkedHashSet<>())) {
34+
String message = format("OneOf Input Object %s must be inhabited but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.", inputObjectType.getName());
35+
errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.OneOfNotInhabited, message));
36+
}
37+
return TraversalControl.CONTINUE;
38+
}
39+
40+
private boolean canBeProvidedAFiniteValue(GraphQLInputObjectType oneOfInputObject, Set<GraphQLInputObjectType> visited) {
41+
if (visited.contains(oneOfInputObject)) {
42+
return false;
43+
}
44+
Set<GraphQLInputObjectType> nextVisited = new LinkedHashSet<>(visited);
45+
nextVisited.add(oneOfInputObject);
46+
for (GraphQLInputObjectField field : oneOfInputObject.getFieldDefinitions()) {
47+
GraphQLType fieldType = field.getType();
48+
if (GraphQLTypeUtil.isList(fieldType)) {
49+
return true;
50+
}
51+
GraphQLUnmodifiedType namedFieldType = GraphQLTypeUtil.unwrapAll(fieldType);
52+
if (!(namedFieldType instanceof GraphQLInputObjectType)) {
53+
return true;
54+
}
55+
GraphQLInputObjectType inputFieldType = (GraphQLInputObjectType) namedFieldType;
56+
if (!inputFieldType.isOneOf()) {
57+
return true;
58+
}
59+
if (canBeProvidedAFiniteValue(inputFieldType, nextVisited)) {
60+
return true;
61+
}
62+
}
63+
return false;
64+
}
65+
2266
@Override
2367
public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField inputObjectField, TraverserContext<GraphQLSchemaElement> context) {
2468
GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) context.getParentNode();

src/main/java/graphql/schema/validation/SchemaValidationErrorType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public enum SchemaValidationErrorType implements SchemaValidationErrorClassifica
2222
InputTypeUsedInOutputTypeContext,
2323
OneOfDefaultValueOnField,
2424
OneOfNonNullableField,
25+
OneOfNotInhabited,
2526
RequiredInputFieldCannotBeDeprecated,
2627
RequiredFieldArgumentCannotBeDeprecated,
2728
RequiredDirectiveArgumentCannotBeDeprecated

src/test/groovy/graphql/schema/validation/OneOfInputObjectRulesTest.groovy

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,243 @@ class OneOfInputObjectRulesTest extends Specification {
3333
schemaProblem.errors[1].description == "OneOf input field OneOfInputType.badDefaulted cannot have a default value."
3434
schemaProblem.errors[1].classification == SchemaValidationErrorType.OneOfDefaultValueOnField
3535
}
36+
37+
def "oneOf with scalar fields is inhabited"() {
38+
def sdl = """
39+
type Query {
40+
f(arg : A) : String
41+
}
42+
43+
input A @oneOf {
44+
a : String
45+
b : Int
46+
}
47+
"""
48+
49+
when:
50+
def registry = new SchemaParser().parse(sdl)
51+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
52+
53+
then:
54+
noExceptionThrown()
55+
}
56+
57+
def "oneOf with enum field is inhabited"() {
58+
def sdl = """
59+
type Query {
60+
f(arg : A) : String
61+
}
62+
63+
enum Color {
64+
RED
65+
GREEN
66+
BLUE
67+
}
68+
69+
input A @oneOf {
70+
a : Color
71+
}
72+
"""
73+
74+
when:
75+
def registry = new SchemaParser().parse(sdl)
76+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
77+
78+
then:
79+
noExceptionThrown()
80+
}
81+
82+
def "oneOf with list field is inhabited"() {
83+
def sdl = """
84+
type Query {
85+
f(arg : A) : String
86+
}
87+
88+
input A @oneOf {
89+
a : [A]
90+
}
91+
"""
92+
93+
when:
94+
def registry = new SchemaParser().parse(sdl)
95+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
96+
97+
then:
98+
noExceptionThrown()
99+
}
100+
101+
def "oneOf referencing non-oneOf input is inhabited"() {
102+
def sdl = """
103+
type Query {
104+
f(arg : A) : String
105+
}
106+
107+
input RegularInput {
108+
x : String
109+
}
110+
111+
input A @oneOf {
112+
a : RegularInput
113+
}
114+
"""
115+
116+
when:
117+
def registry = new SchemaParser().parse(sdl)
118+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
119+
120+
then:
121+
noExceptionThrown()
122+
}
123+
124+
def "oneOf with escape field is inhabited"() {
125+
def sdl = """
126+
type Query {
127+
f(arg : A) : String
128+
}
129+
130+
input A @oneOf {
131+
b : B
132+
escape : String
133+
}
134+
135+
input B @oneOf {
136+
a : A
137+
}
138+
"""
139+
140+
when:
141+
def registry = new SchemaParser().parse(sdl)
142+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
143+
144+
then:
145+
noExceptionThrown()
146+
}
147+
148+
def "mutually referencing oneOf types with scalar escape is inhabited"() {
149+
def sdl = """
150+
type Query {
151+
f(arg : A) : String
152+
}
153+
154+
input A @oneOf {
155+
b : B
156+
}
157+
158+
input B @oneOf {
159+
a : A
160+
escape : Int
161+
}
162+
"""
163+
164+
when:
165+
def registry = new SchemaParser().parse(sdl)
166+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
167+
168+
then:
169+
noExceptionThrown()
170+
}
171+
172+
def "oneOf referencing non-oneOf with back-reference is inhabited"() {
173+
def sdl = """
174+
type Query {
175+
f(arg : A) : String
176+
}
177+
178+
input A @oneOf {
179+
b : RegularInput
180+
}
181+
182+
input RegularInput {
183+
back : A
184+
}
185+
"""
186+
187+
when:
188+
def registry = new SchemaParser().parse(sdl)
189+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
190+
191+
then:
192+
noExceptionThrown()
193+
}
194+
195+
def "multiple fields with chained oneOf escape is inhabited"() {
196+
def sdl = """
197+
type Query {
198+
f(arg : A) : String
199+
}
200+
201+
input A @oneOf {
202+
b : B
203+
c : C
204+
}
205+
206+
input B @oneOf {
207+
a : A
208+
}
209+
210+
input C @oneOf {
211+
a : A
212+
escape : String
213+
}
214+
"""
215+
216+
when:
217+
def registry = new SchemaParser().parse(sdl)
218+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
219+
220+
then:
221+
noExceptionThrown()
222+
}
223+
224+
def "single oneOf self-reference cycle is not inhabited"() {
225+
def sdl = """
226+
type Query {
227+
f(arg : A) : String
228+
}
229+
230+
input A @oneOf {
231+
self : A
232+
}
233+
"""
234+
235+
when:
236+
def registry = new SchemaParser().parse(sdl)
237+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
238+
239+
then:
240+
def schemaProblem = thrown(InvalidSchemaException)
241+
schemaProblem.errors.size() == 1
242+
schemaProblem.errors[0].classification == SchemaValidationErrorType.OneOfNotInhabited
243+
}
244+
245+
def "multiple oneOf types forming cycle are not inhabited"() {
246+
def sdl = """
247+
type Query {
248+
f(arg : A) : String
249+
}
250+
251+
input A @oneOf {
252+
b : B
253+
}
254+
255+
input B @oneOf {
256+
c : C
257+
}
258+
259+
input C @oneOf {
260+
a : A
261+
}
262+
"""
263+
264+
when:
265+
def registry = new SchemaParser().parse(sdl)
266+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
267+
268+
then:
269+
def schemaProblem = thrown(InvalidSchemaException)
270+
schemaProblem.errors.size() == 3
271+
schemaProblem.errors[0].classification == SchemaValidationErrorType.OneOfNotInhabited
272+
schemaProblem.errors[1].classification == SchemaValidationErrorType.OneOfNotInhabited
273+
schemaProblem.errors[2].classification == SchemaValidationErrorType.OneOfNotInhabited
274+
}
36275
}

0 commit comments

Comments
 (0)