Skip to content

Commit fd7ff0a

Browse files
authored
Merge pull request #4248 from jbellenger/jbellenger-oneof-inhabitability
2 parents bd87652 + 289505e commit fd7ff0a

File tree

3 files changed

+199
-0
lines changed

3 files changed

+199
-0
lines changed

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: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,158 @@ 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 { f(arg: A): String }
40+
input A @oneOf { a: String, b: Int }
41+
"""
42+
43+
when:
44+
def registry = new SchemaParser().parse(sdl)
45+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
46+
47+
then:
48+
noExceptionThrown()
49+
}
50+
51+
def "oneOf with enum field is inhabited"() {
52+
def sdl = """
53+
type Query { f(arg: A): String }
54+
enum Color { RED GREEN BLUE }
55+
input A @oneOf { a: Color }
56+
"""
57+
58+
when:
59+
def registry = new SchemaParser().parse(sdl)
60+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
61+
62+
then:
63+
noExceptionThrown()
64+
}
65+
66+
def "oneOf with list field is inhabited"() {
67+
def sdl = """
68+
type Query { f(arg: A): String }
69+
input A @oneOf { a: [A] }
70+
"""
71+
72+
when:
73+
def registry = new SchemaParser().parse(sdl)
74+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
75+
76+
then:
77+
noExceptionThrown()
78+
}
79+
80+
def "oneOf referencing non-oneOf input is inhabited"() {
81+
def sdl = """
82+
type Query { f(arg: A): String }
83+
input A @oneOf { a: RegularInput }
84+
input RegularInput { x: String }
85+
"""
86+
87+
when:
88+
def registry = new SchemaParser().parse(sdl)
89+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
90+
91+
then:
92+
noExceptionThrown()
93+
}
94+
95+
def "oneOf with escape field is inhabited"() {
96+
def sdl = """
97+
type Query { f(arg: A): String }
98+
input A @oneOf { b: B, escape: String }
99+
input B @oneOf { a: A }
100+
"""
101+
102+
when:
103+
def registry = new SchemaParser().parse(sdl)
104+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
105+
106+
then:
107+
noExceptionThrown()
108+
}
109+
110+
def "mutually referencing oneOf types with scalar escape is inhabited"() {
111+
def sdl = """
112+
type Query { f(arg: A): String }
113+
input A @oneOf { b: B }
114+
input B @oneOf { a: A, escape: Int }
115+
"""
116+
117+
when:
118+
def registry = new SchemaParser().parse(sdl)
119+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
120+
121+
then:
122+
noExceptionThrown()
123+
}
124+
125+
def "oneOf referencing non-oneOf with back-reference is inhabited"() {
126+
def sdl = """
127+
type Query { f(arg: A): String }
128+
input A @oneOf { b: RegularInput }
129+
input RegularInput { back: A }
130+
"""
131+
132+
when:
133+
def registry = new SchemaParser().parse(sdl)
134+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
135+
136+
then:
137+
noExceptionThrown()
138+
}
139+
140+
def "multiple fields with chained oneOf escape is inhabited"() {
141+
def sdl = """
142+
type Query { f(arg: A): String }
143+
input A @oneOf { b: B, c: C }
144+
input B @oneOf { a: A }
145+
input C @oneOf { a: A, escape: String }
146+
"""
147+
148+
when:
149+
def registry = new SchemaParser().parse(sdl)
150+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
151+
152+
then:
153+
noExceptionThrown()
154+
}
155+
156+
def "single oneOf self-reference cycle is not inhabited"() {
157+
def sdl = """
158+
type Query { f(arg: A): String }
159+
input A @oneOf { self: A }
160+
"""
161+
162+
when:
163+
def registry = new SchemaParser().parse(sdl)
164+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
165+
166+
then:
167+
def schemaProblem = thrown(InvalidSchemaException)
168+
schemaProblem.errors.size() == 1
169+
schemaProblem.errors[0].description == "OneOf Input Object A must be inhabited but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle."
170+
schemaProblem.errors[0].classification == SchemaValidationErrorType.OneOfNotInhabited
171+
}
172+
173+
def "multiple oneOf types forming cycle are not inhabited"() {
174+
def sdl = """
175+
type Query { f(arg: A): String }
176+
input A @oneOf { b: B }
177+
input B @oneOf { c: C }
178+
input C @oneOf { a: A }
179+
"""
180+
181+
when:
182+
def registry = new SchemaParser().parse(sdl)
183+
new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring())
184+
185+
then:
186+
def schemaProblem = thrown(InvalidSchemaException)
187+
schemaProblem.errors.size() == 3
188+
schemaProblem.errors.every { it.classification == SchemaValidationErrorType.OneOfNotInhabited }
189+
}
36190
}

0 commit comments

Comments
 (0)