Skip to content

Commit fe28db5

Browse files
authored
Merge pull request graphql-java#3275 from graphql-java/oneOf_support_branch
Experimental - oneOf directive support
2 parents ba589a8 + 12a00d1 commit fe28db5

30 files changed

+684
-27
lines changed

src/main/java/graphql/Directives.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import static graphql.introspection.Introspection.DirectiveLocation.FRAGMENT_SPREAD;
1616
import static graphql.introspection.Introspection.DirectiveLocation.INLINE_FRAGMENT;
1717
import static graphql.introspection.Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION;
18+
import static graphql.introspection.Introspection.DirectiveLocation.INPUT_OBJECT;
1819
import static graphql.introspection.Introspection.DirectiveLocation.SCALAR;
1920
import static graphql.language.DirectiveLocation.newDirectiveLocation;
2021
import static graphql.language.InputValueDefinition.newInputValueDefinition;
@@ -31,10 +32,13 @@ public class Directives {
3132

3233
private static final String SPECIFIED_BY = "specifiedBy";
3334
private static final String DEPRECATED = "deprecated";
35+
private static final String ONE_OF = "oneOf";
3436

3537
public static final String NO_LONGER_SUPPORTED = "No longer supported";
3638
public static final DirectiveDefinition DEPRECATED_DIRECTIVE_DEFINITION;
3739
public static final DirectiveDefinition SPECIFIED_BY_DIRECTIVE_DEFINITION;
40+
@ExperimentalApi
41+
public static final DirectiveDefinition ONE_OF_DIRECTIVE_DEFINITION;
3842

3943

4044
static {
@@ -65,6 +69,12 @@ public class Directives {
6569
.type(newNonNullType(newTypeName().name("String").build()).build())
6670
.build())
6771
.build();
72+
73+
ONE_OF_DIRECTIVE_DEFINITION = DirectiveDefinition.newDirectiveDefinition()
74+
.name(ONE_OF)
75+
.directiveLocation(newDirectiveLocation().name(INPUT_OBJECT.name()).build())
76+
.description(createDescription("Indicates an Input Object is a OneOf Input Object."))
77+
.build();
6878
}
6979

7080
public static final GraphQLDirective IncludeDirective = GraphQLDirective.newDirective()
@@ -119,6 +129,14 @@ public class Directives {
119129
.definition(SPECIFIED_BY_DIRECTIVE_DEFINITION)
120130
.build();
121131

132+
@ExperimentalApi
133+
public static final GraphQLDirective OneOfDirective = GraphQLDirective.newDirective()
134+
.name(ONE_OF)
135+
.description("Indicates an Input Object is a OneOf Input Object.")
136+
.validLocations(INPUT_OBJECT)
137+
.definition(ONE_OF_DIRECTIVE_DEFINITION)
138+
.build();
139+
122140
private static Description createDescription(String s) {
123141
return new Description(s, null, false);
124142
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package graphql.execution;
2+
3+
import graphql.ErrorType;
4+
import graphql.GraphQLError;
5+
import graphql.GraphQLException;
6+
import graphql.PublicApi;
7+
import graphql.language.SourceLocation;
8+
9+
import java.util.List;
10+
11+
/**
12+
* The input map to One Of Input Types MUST only have 1 entry with a non null value
13+
*/
14+
@PublicApi
15+
public class OneOfNullValueException extends GraphQLException implements GraphQLError {
16+
17+
public OneOfNullValueException(String message) {
18+
super(message);
19+
}
20+
21+
@Override
22+
public List<SourceLocation> getLocations() {
23+
return null;
24+
}
25+
26+
@Override
27+
public ErrorType getErrorType() {
28+
return ErrorType.ValidationError;
29+
}
30+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package graphql.execution;
2+
3+
import graphql.ErrorType;
4+
import graphql.GraphQLError;
5+
import graphql.GraphQLException;
6+
import graphql.PublicApi;
7+
import graphql.language.SourceLocation;
8+
import graphql.schema.GraphQLType;
9+
import graphql.schema.GraphQLTypeUtil;
10+
11+
import java.util.List;
12+
13+
/**
14+
* The input map to One Of Input Types MUST only have 1 entry
15+
*/
16+
@PublicApi
17+
public class OneOfTooManyKeysException extends GraphQLException implements GraphQLError {
18+
19+
public OneOfTooManyKeysException(String message) {
20+
super(message);
21+
}
22+
23+
@Override
24+
public List<SourceLocation> getLocations() {
25+
return null;
26+
}
27+
28+
@Override
29+
public ErrorType getErrorType() {
30+
return ErrorType.ValidationError;
31+
}
32+
}

src/main/java/graphql/execution/ValuesResolver.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package graphql.execution;
22

33

4+
import graphql.Assert;
45
import graphql.GraphQLContext;
56
import graphql.Internal;
67
import graphql.collect.ImmutableKit;
78
import graphql.execution.values.InputInterceptor;
9+
import graphql.i18n.I18n;
810
import graphql.language.Argument;
911
import graphql.language.ArrayValue;
1012
import graphql.language.NullValue;
@@ -373,12 +375,42 @@ private static Map<String, Object> getArgumentValuesImpl(
373375
locale);
374376
coercedValues.put(argumentName, value);
375377
}
378+
// @oneOf input must be checked now that all variables and literals have been converted
379+
if (argumentType instanceof GraphQLInputObjectType) {
380+
GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) argumentType;
381+
if (inputObjectType.isOneOf() && ! ValuesResolverConversion.isNullValue(value)) {
382+
validateOneOfInputTypes(inputObjectType, argumentValue, argumentName, value, locale);
383+
}
384+
}
376385
}
377386

378387
}
379388
return coercedValues;
380389
}
381390

391+
@SuppressWarnings("unchecked")
392+
private static void validateOneOfInputTypes(GraphQLInputObjectType oneOfInputType, Value argumentValue, String argumentName, Object inputValue, Locale locale) {
393+
Assert.assertTrue(inputValue instanceof Map, () -> String.format("The coerced argument %s GraphQLInputObjectType is unexpectedly not a map", argumentName));
394+
Map<String, Object> objectMap = (Map<String, Object>) inputValue;
395+
int mapSize;
396+
if (argumentValue instanceof ObjectValue) {
397+
mapSize = ((ObjectValue) argumentValue).getObjectFields().size();
398+
} else {
399+
mapSize = objectMap.size();
400+
}
401+
if (mapSize != 1) {
402+
String msg = I18n.i18n(I18n.BundleType.Execution, locale)
403+
.msg("Execution.handleOneOfNotOneFieldError", oneOfInputType.getName());
404+
throw new OneOfTooManyKeysException(msg);
405+
}
406+
String fieldName = objectMap.keySet().iterator().next();
407+
if (objectMap.get(fieldName) == null) {
408+
String msg = I18n.i18n(I18n.BundleType.Execution, locale)
409+
.msg("Execution.handleOneOfValueIsNullError", oneOfInputType.getName() + "." + fieldName);
410+
throw new OneOfNullValueException(msg);
411+
}
412+
}
413+
382414
private static Map<String, Argument> argumentMap(List<Argument> arguments) {
383415
Map<String, Argument> result = new LinkedHashMap<>(arguments.size());
384416
for (Argument argument : arguments) {

src/main/java/graphql/introspection/Introspection.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,14 @@ private static String printDefaultValue(InputValueWithState inputValueWithState,
399399
return null;
400400
};
401401

402+
private static final IntrospectionDataFetcher<?> isOneOfFetcher = environment -> {
403+
Object type = environment.getSource();
404+
if (type instanceof GraphQLInputObjectType) {
405+
return ((GraphQLInputObjectType) type).isOneOf();
406+
}
407+
return null;
408+
};
409+
402410
public static final GraphQLObjectType __Type = newObject()
403411
.name("__Type")
404412
.field(newFieldDefinition()
@@ -440,6 +448,10 @@ private static String printDefaultValue(InputValueWithState inputValueWithState,
440448
.field(newFieldDefinition()
441449
.name("ofType")
442450
.type(typeRef("__Type")))
451+
.field(newFieldDefinition()
452+
.name("isOneOf")
453+
.description("This field is considered experimental because it has not yet been ratified in the graphql specification")
454+
.type(GraphQLBoolean))
443455
.field(newFieldDefinition()
444456
.name("specifiedByURL")
445457
.type(GraphQLString))
@@ -460,6 +472,7 @@ private static String printDefaultValue(InputValueWithState inputValueWithState,
460472
register(__Type, "enumValues", enumValuesTypesFetcher);
461473
register(__Type, "inputFields", inputFieldsFetcher);
462474
register(__Type, "ofType", OfTypeFetcher);
475+
register(__Type, "isOneOf", isOneOfFetcher);
463476
register(__Type, "specifiedByURL", specifiedByUrlDataFetcher);
464477
register(__Type, "specifiedByUrl", specifiedByUrlDataFetcher); // note that this field is deprecated
465478
}

src/main/java/graphql/introspection/IntrospectionQueryBuilder.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public static class Options {
3333
private final boolean descriptions;
3434

3535
private final boolean specifiedByUrl;
36+
private final boolean isOneOf;
3637

3738
private final boolean directiveIsRepeatable;
3839

@@ -44,12 +45,14 @@ public static class Options {
4445

4546
private Options(boolean descriptions,
4647
boolean specifiedByUrl,
48+
boolean isOneOf,
4749
boolean directiveIsRepeatable,
4850
boolean schemaDescription,
4951
boolean inputValueDeprecation,
5052
int typeRefFragmentDepth) {
5153
this.descriptions = descriptions;
5254
this.specifiedByUrl = specifiedByUrl;
55+
this.isOneOf = isOneOf;
5356
this.directiveIsRepeatable = directiveIsRepeatable;
5457
this.schemaDescription = schemaDescription;
5558
this.inputValueDeprecation = inputValueDeprecation;
@@ -64,6 +67,10 @@ public boolean isSpecifiedByUrl() {
6467
return specifiedByUrl;
6568
}
6669

70+
public boolean isOneOf() {
71+
return isOneOf;
72+
}
73+
6774
public boolean isDirectiveIsRepeatable() {
6875
return directiveIsRepeatable;
6976
}
@@ -85,6 +92,7 @@ public static Options defaultOptions() {
8592
true,
8693
false,
8794
true,
95+
true,
8896
false,
8997
true,
9098
7
@@ -101,6 +109,7 @@ public static Options defaultOptions() {
101109
public Options descriptions(boolean flag) {
102110
return new Options(flag,
103111
this.specifiedByUrl,
112+
this.isOneOf,
104113
this.directiveIsRepeatable,
105114
this.schemaDescription,
106115
this.inputValueDeprecation,
@@ -117,12 +126,32 @@ public Options descriptions(boolean flag) {
117126
public Options specifiedByUrl(boolean flag) {
118127
return new Options(this.descriptions,
119128
flag,
129+
this.isOneOf,
120130
this.directiveIsRepeatable,
121131
this.schemaDescription,
122132
this.inputValueDeprecation,
123133
this.typeRefFragmentDepth);
124134
}
125135

136+
/**
137+
* This will allow you to include the `isOneOf` field for one of input types in the introspection query.
138+
* <p>
139+
* This option is only needed while `@oneOf` input types are new and in time the reason for this
140+
* option will go away.
141+
*
142+
* @param flag whether to include them
143+
*
144+
* @return options
145+
*/
146+
public Options isOneOf(boolean flag) {
147+
return new Options(this.descriptions,
148+
this.specifiedByUrl,
149+
flag,
150+
this.directiveIsRepeatable,
151+
this.schemaDescription,
152+
this.inputValueDeprecation,
153+
this.typeRefFragmentDepth);
154+
}
126155
/**
127156
* This will allow you to include the `isRepeatable` field for directives in the introspection query.
128157
*
@@ -133,6 +162,7 @@ public Options specifiedByUrl(boolean flag) {
133162
public Options directiveIsRepeatable(boolean flag) {
134163
return new Options(this.descriptions,
135164
this.specifiedByUrl,
165+
this.isOneOf,
136166
flag,
137167
this.schemaDescription,
138168
this.inputValueDeprecation,
@@ -149,6 +179,7 @@ public Options directiveIsRepeatable(boolean flag) {
149179
public Options schemaDescription(boolean flag) {
150180
return new Options(this.descriptions,
151181
this.specifiedByUrl,
182+
this.isOneOf,
152183
this.directiveIsRepeatable,
153184
flag,
154185
this.inputValueDeprecation,
@@ -165,6 +196,7 @@ public Options schemaDescription(boolean flag) {
165196
public Options inputValueDeprecation(boolean flag) {
166197
return new Options(this.descriptions,
167198
this.specifiedByUrl,
199+
this.isOneOf,
168200
this.directiveIsRepeatable,
169201
this.schemaDescription,
170202
flag,
@@ -181,6 +213,7 @@ public Options inputValueDeprecation(boolean flag) {
181213
public Options typeRefFragmentDepth(int typeRefFragmentDepth) {
182214
return new Options(this.descriptions,
183215
this.specifiedByUrl,
216+
this.isOneOf,
184217
this.directiveIsRepeatable,
185218
this.schemaDescription,
186219
this.inputValueDeprecation,
@@ -249,6 +282,7 @@ public static Document buildDocument(Options options) {
249282
newField("name").build(),
250283
options.descriptions ? newField("description").build() : null,
251284
options.specifiedByUrl ? newField("specifiedByURL").build() : null,
285+
options.isOneOf ? newField("isOneOf").build() : null,
252286
newField("fields")
253287
.arguments(ImmutableList.of(
254288
newArgument("includeDeprecated", BooleanValue.of(true)).build()

src/main/java/graphql/language/VariableReference.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ public TraversalControl accept(TraverserContext<Node> context, NodeVisitor visit
9191
return visitor.visitVariableReference(this, context);
9292
}
9393

94+
public static VariableReference of(String name) {
95+
return newVariableReference().name(name).build();
96+
}
97+
9498
public static Builder newVariableReference() {
9599
return new Builder();
96100
}

src/main/java/graphql/schema/GraphQLInputObjectType.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import com.google.common.collect.ImmutableList;
44
import com.google.common.collect.ImmutableMap;
5+
import graphql.Directives;
56
import graphql.DirectivesUtil;
7+
import graphql.ExperimentalApi;
68
import graphql.Internal;
79
import graphql.PublicApi;
810
import graphql.language.InputObjectTypeDefinition;
@@ -34,6 +36,7 @@
3436
public class GraphQLInputObjectType implements GraphQLNamedInputType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLInputFieldsContainer, GraphQLDirectiveContainer {
3537

3638
private final String name;
39+
private final boolean isOneOf;
3740
private final String description;
3841
private final ImmutableMap<String, GraphQLInputObjectField> fieldMap;
3942
private final InputObjectTypeDefinition definition;
@@ -60,18 +63,41 @@ private GraphQLInputObjectType(String name,
6063
this.extensionDefinitions = ImmutableList.copyOf(extensionDefinitions);
6164
this.directives = new DirectivesUtil.DirectivesHolder(directives, appliedDirectives);
6265
this.fieldMap = buildDefinitionMap(fields);
66+
this.isOneOf = hasOneOf(directives, appliedDirectives);
6367
}
6468

6569
private ImmutableMap<String, GraphQLInputObjectField> buildDefinitionMap(List<GraphQLInputObjectField> fieldDefinitions) {
6670
return ImmutableMap.copyOf(FpKit.getByName(fieldDefinitions, GraphQLInputObjectField::getName,
6771
(fld1, fld2) -> assertShouldNeverHappen("Duplicated definition for field '%s' in type '%s'", fld1.getName(), this.name)));
6872
}
6973

74+
private boolean hasOneOf(List<GraphQLDirective> directives, List<GraphQLAppliedDirective> appliedDirectives) {
75+
if (appliedDirectives.stream().anyMatch(d -> Directives.OneOfDirective.getName().equals(d.getName()))) {
76+
return true;
77+
}
78+
// eventually GraphQLDirective as applied directive goes away
79+
return directives.stream().anyMatch(d -> Directives.OneOfDirective.getName().equals(d.getName()));
80+
}
81+
7082
@Override
7183
public String getName() {
7284
return name;
7385
}
7486

87+
88+
/**
89+
* An Input Object is considered a OneOf Input Object if it has the `@oneOf` directive applied to it.
90+
* <p>
91+
* This API is currently considered experimental since the graphql specification has not yet ratified
92+
* this approach.
93+
*
94+
* @return true if it's a OneOf Input Object
95+
*/
96+
@ExperimentalApi
97+
public boolean isOneOf() {
98+
return isOneOf;
99+
}
100+
75101
public String getDescription() {
76102
return description;
77103
}

0 commit comments

Comments
 (0)