Skip to content

Commit ff3f691

Browse files
authored
Repeatable directives support (#2015)
* Repeatable directives support Still much to do * Repeatable directives support Introspection and better grammar plus GraphlXX runtime types * fixed test * Better documentation and more tests * Now has runtime types included * Fixing up tests * SchemaPrinter and AstPrinter support * Fixing builds after bad merge * PR feedback * Removed the getDirectiveByName on AST area * Killed nonRepeatableDirectivesByName * Killed nonRepeatableDirectivesByName * Added extra tests for printing repeatable directives * Moved to a directive holder
1 parent fb86ee6 commit ff3f691

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1181
-359
lines changed

src/main/antlr/GraphqlCommon.g4

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ arguments : '(' argument+ ')';
2929

3030
argument : name ':' valueWithVariable;
3131

32-
baseName: NAME | FRAGMENT | QUERY | MUTATION | SUBSCRIPTION | SCHEMA | SCALAR | TYPE | INTERFACE | IMPLEMENTS | ENUM | UNION | INPUT | EXTEND | DIRECTIVE;
32+
baseName: NAME | FRAGMENT | QUERY | MUTATION | SUBSCRIPTION | SCHEMA | SCALAR | TYPE | INTERFACE | IMPLEMENTS | ENUM | UNION | INPUT | EXTEND | DIRECTIVE | REPEATABLE;
3333
fragmentName: baseName | BooleanValue | NullValue;
3434
enumValueName: baseName | ON_KEYWORD;
3535

@@ -88,6 +88,7 @@ INPUT: 'input';
8888
EXTEND: 'extend';
8989
DIRECTIVE: 'directive';
9090
ON_KEYWORD: 'on';
91+
REPEATABLE: 'repeatable';
9192
NAME: [_A-Za-z][_0-9A-Za-z]*;
9293

9394

src/main/antlr/GraphqlSDL.g4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ inputObjectValueDefinitions : '{' inputValueDefinition* '}';
121121
extensionInputObjectValueDefinitions : '{' inputValueDefinition+ '}';
122122

123123

124-
directiveDefinition : description? DIRECTIVE '@' name argumentsDefinition? 'on' directiveLocations;
124+
directiveDefinition : description? DIRECTIVE '@' name argumentsDefinition? REPEATABLE? ON_KEYWORD directiveLocations;
125125

126126
directiveLocation : name;
127127

src/main/java/graphql/Directives.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public class Directives {
6262
.build())
6363
.build();
6464
}
65+
6566
public static final GraphQLDirective IncludeDirective = GraphQLDirective.newDirective()
6667
.name("include")
6768
.description("Directs the executor to include this field or fragment only when the `if` argument is true")
Lines changed: 127 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,152 @@
11
package graphql;
22

3+
import com.google.common.collect.ImmutableList;
4+
import com.google.common.collect.ImmutableMap;
35
import graphql.schema.GraphQLArgument;
46
import graphql.schema.GraphQLDirective;
57
import graphql.util.FpKit;
68

9+
import java.util.Collection;
710
import java.util.List;
811
import java.util.Map;
912
import java.util.Optional;
13+
import java.util.stream.Collectors;
14+
15+
import static graphql.Assert.assertNotNull;
16+
import static graphql.collect.ImmutableKit.emptyList;
1017

1118
@Internal
1219
public class DirectivesUtil {
1320

14-
public static Map<String, GraphQLDirective> directivesByName(List<GraphQLDirective> directiveList) {
15-
return FpKit.getByName(directiveList, GraphQLDirective::getName, FpKit.mergeFirst());
21+
22+
public static Map<String, GraphQLDirective> nonRepeatableDirectivesByName(List<GraphQLDirective> directives) {
23+
// filter the repeatable directives
24+
List<GraphQLDirective> singletonDirectives = directives.stream()
25+
.filter(d -> !d.isRepeatable()).collect(Collectors.toList());
26+
27+
return FpKit.getByName(singletonDirectives, GraphQLDirective::getName);
1628
}
1729

18-
public static Optional<GraphQLDirective> directiveByName(List<GraphQLDirective> directives, String directiveName) {
19-
for (GraphQLDirective directive : directives) {
20-
if (directive.getName().equals(directiveName)) {
21-
return Optional.of(directive);
22-
}
30+
public static Map<String, ImmutableList<GraphQLDirective>> allDirectivesByName(List<GraphQLDirective> directives) {
31+
32+
return ImmutableMap.copyOf(FpKit.groupingBy(directives, GraphQLDirective::getName));
33+
}
34+
35+
public static GraphQLDirective nonRepeatedDirectiveByNameWithAssert(Map<String, List<GraphQLDirective>> directives, String directiveName) {
36+
List<GraphQLDirective> directiveList = directives.get(directiveName);
37+
if (directiveList == null || directiveList.isEmpty()) {
38+
return null;
2339
}
24-
return Optional.empty();
40+
Assert.assertTrue(isAllNonRepeatable(directiveList), () -> String.format("'%s' is a repeatable directive and you have used a non repeatable access method", directiveName));
41+
return directiveList.get(0);
2542
}
2643

27-
public static Optional<GraphQLArgument> directiveWithArg(List<GraphQLDirective> directiveList, String directiveName, String argumentName) {
28-
GraphQLDirective directive = directiveByName(directiveList, directiveName).orElse(null);
44+
public static Optional<GraphQLArgument> directiveWithArg(List<GraphQLDirective> directives, String directiveName, String argumentName) {
45+
GraphQLDirective directive = nonRepeatableDirectivesByName(directives).get(directiveName);
2946
GraphQLArgument argument = null;
3047
if (directive != null) {
3148
argument = directive.getArgument(argumentName);
3249
}
3350
return Optional.ofNullable(argument);
3451
}
52+
53+
54+
public static boolean isAllNonRepeatable(List<GraphQLDirective> directives) {
55+
if (directives == null || directives.isEmpty()) {
56+
return false;
57+
}
58+
for (GraphQLDirective graphQLDirective : directives) {
59+
if (graphQLDirective.isRepeatable()) {
60+
return false;
61+
}
62+
}
63+
return true;
64+
}
65+
66+
public static List<GraphQLDirective> enforceAdd(List<GraphQLDirective> targetList, GraphQLDirective newDirective) {
67+
assertNotNull(targetList, () -> "directive list can't be null");
68+
assertNotNull(newDirective, () -> "directive can't be null");
69+
70+
// check whether the newDirective is repeatable in advance, to avoid needless operations
71+
if (newDirective.isNonRepeatable()) {
72+
Map<String, ImmutableList<GraphQLDirective>> map = allDirectivesByName(targetList);
73+
assertNonRepeatable(newDirective, map);
74+
}
75+
targetList.add(newDirective);
76+
return targetList;
77+
}
78+
79+
public static List<GraphQLDirective> enforceAddAll(List<GraphQLDirective> targetList, List<GraphQLDirective> newDirectives) {
80+
assertNotNull(targetList, () -> "directive list can't be null");
81+
assertNotNull(newDirectives, () -> "directive list can't be null");
82+
Map<String, ImmutableList<GraphQLDirective>> map = allDirectivesByName(targetList);
83+
for (GraphQLDirective newDirective : newDirectives) {
84+
assertNonRepeatable(newDirective, map);
85+
targetList.add(newDirective);
86+
}
87+
return targetList;
88+
}
89+
90+
private static void assertNonRepeatable(GraphQLDirective directive, Map<String, ImmutableList<GraphQLDirective>> mapOfDirectives) {
91+
if (directive.isNonRepeatable()) {
92+
List<GraphQLDirective> currentDirectives = mapOfDirectives.getOrDefault(directive.getName(), emptyList());
93+
int currentSize = currentDirectives.size();
94+
if (currentSize > 0) {
95+
Assert.assertShouldNeverHappen("%s is a non repeatable directive but there is already one present in this list", directive.getName());
96+
}
97+
}
98+
}
99+
100+
public static GraphQLDirective getFirstDirective(String name, Map<String, List<GraphQLDirective>> allDirectivesByName) {
101+
List<GraphQLDirective> directives = allDirectivesByName.getOrDefault(name, emptyList());
102+
if (directives.isEmpty()) {
103+
return null;
104+
}
105+
return directives.get(0);
106+
}
107+
108+
/**
109+
* A holder class that breaks a list of directives into maps to be more easily accessible in using classes
110+
*/
111+
public static class DirectivesHolder {
112+
113+
private final ImmutableMap<String, List<GraphQLDirective>> allDirectivesByName;
114+
private final ImmutableMap<String, GraphQLDirective> nonRepeatableDirectivesByName;
115+
private final List<GraphQLDirective> allDirectives;
116+
117+
public DirectivesHolder(Collection<GraphQLDirective> allDirectives) {
118+
this.allDirectives = ImmutableList.copyOf(allDirectives);
119+
this.allDirectivesByName = ImmutableMap.copyOf(FpKit.groupingBy(allDirectives, GraphQLDirective::getName));
120+
// filter out the repeatable directives
121+
List<GraphQLDirective> nonRepeatableDirectives = allDirectives.stream()
122+
.filter(d -> !d.isRepeatable()).collect(Collectors.toList());
123+
this.nonRepeatableDirectivesByName = ImmutableMap.copyOf(FpKit.getByName(nonRepeatableDirectives, GraphQLDirective::getName));
124+
}
125+
126+
public ImmutableMap<String, List<GraphQLDirective>> getAllDirectivesByName() {
127+
return allDirectivesByName;
128+
}
129+
130+
public ImmutableMap<String, GraphQLDirective> getDirectivesByName() {
131+
return nonRepeatableDirectivesByName;
132+
}
133+
134+
public List<GraphQLDirective> getDirectives() {
135+
return allDirectives;
136+
}
137+
138+
public GraphQLDirective getDirective(String directiveName) {
139+
List<GraphQLDirective> directiveList = allDirectivesByName.get(directiveName);
140+
if (directiveList == null || directiveList.isEmpty()) {
141+
return null;
142+
}
143+
Assert.assertTrue(isAllNonRepeatable(directiveList), () -> String.format("'%s' is a repeatable directive and you have used a non repeatable access method", directiveName));
144+
return directiveList.get(0);
145+
146+
}
147+
148+
public List<GraphQLDirective> getDirectives(String directiveName) {
149+
return allDirectivesByName.getOrDefault(directiveName, emptyList());
150+
}
151+
}
35152
}

src/main/java/graphql/execution/ConditionalNodes.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
import graphql.Internal;
55
import graphql.VisibleForTesting;
66
import graphql.language.Directive;
7+
import graphql.language.NodeUtil;
78

89
import java.util.List;
910
import java.util.Map;
1011

1112
import static graphql.Directives.IncludeDirective;
1213
import static graphql.Directives.SkipDirective;
13-
import static graphql.language.NodeUtil.directiveByName;
14+
import static graphql.collect.ImmutableKit.emptyList;
1415

1516

1617
@Internal
@@ -26,23 +27,20 @@ public boolean shouldInclude(Map<String, Object> variables, List<Directive> dire
2627
return !skip && include;
2728
}
2829

29-
private Directive getDirectiveByName(List<Directive> directives, String name) {
30-
if (directives.isEmpty()) {
31-
return null;
32-
}
33-
return directiveByName(directives, name).orElse(null);
34-
}
35-
3630
private boolean getDirectiveResult(Map<String, Object> variables, List<Directive> directives, String directiveName, boolean defaultValue) {
37-
Directive directive = getDirectiveByName(directives, directiveName);
38-
if (directive != null) {
31+
List<Directive> foundDirectives = getDirectiveByName(directives, directiveName);
32+
if (!foundDirectives.isEmpty()) {
33+
Directive directive = foundDirectives.get(0);
3934
Map<String, Object> argumentValues = valuesResolver.getArgumentValues(SkipDirective.getArguments(), directive.getArguments(), variables);
4035
Object flag = argumentValues.get("if");
4136
Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", directiveName));
4237
return (Boolean) flag;
4338
}
44-
4539
return defaultValue;
4640
}
4741

42+
private List<Directive> getDirectiveByName(List<Directive> directives, String name) {
43+
return NodeUtil.allDirectivesByName(directives).getOrDefault(name, emptyList());
44+
}
45+
4846
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,13 +435,17 @@ public enum DirectiveLocation {
435435
.name("__Directive")
436436
.field(newFieldDefinition()
437437
.name("name")
438-
.type(GraphQLString))
438+
.description("The __Directive type represents a Directive that a server supports.")
439+
.type(nonNull(GraphQLString)))
439440
.field(newFieldDefinition()
440441
.name("description")
441442
.type(GraphQLString))
443+
.field(newFieldDefinition()
444+
.name("isRepeatable")
445+
.type(nonNull(GraphQLBoolean)))
442446
.field(newFieldDefinition()
443447
.name("locations")
444-
.type(list(nonNull(__DirectiveLocation))))
448+
.type(nonNull(list(nonNull(__DirectiveLocation)))))
445449
.field(newFieldDefinition()
446450
.name("args")
447451
.type(nonNull(list(nonNull(__InputValue)))))

src/main/java/graphql/language/AstPrinter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ private NodePrinter<DirectiveDefinition> directiveDefinition() {
103103
out.printf("%s", description(node));
104104
String arguments = wrap("(", join(node.getInputValueDefinitions(), argSep), ")");
105105
String locations = join(node.getDirectiveLocations(), " | ");
106-
out.printf("directive @%s%s on %s", node.getName(), arguments, locations);
106+
String repeatable = node.isRepeatable() ? "repeatable " : "";
107+
out.printf("directive @%s%s %son %s", node.getName(), arguments, repeatable, locations);
107108
};
108109
}
109110

src/main/java/graphql/language/Directive.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public Map<String, Argument> getArgumentsByName() {
6565
}
6666

6767
public Argument getArgument(String argumentName) {
68-
return getArgumentByName(arguments, argumentName).orElse(null);
68+
return NodeUtil.getArgumentByName(arguments, argumentName);
6969
}
7070

7171
@Override

src/main/java/graphql/language/DirectiveDefinition.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
@PublicApi
2323
public class DirectiveDefinition extends AbstractDescribedNode<DirectiveDefinition> implements SDLDefinition<DirectiveDefinition>, NamedNode<DirectiveDefinition> {
2424
private final String name;
25+
private final boolean repeatable;
2526
private final ImmutableList<InputValueDefinition> inputValueDefinitions;
2627
private final ImmutableList<DirectiveLocation> directiveLocations;
2728

@@ -30,6 +31,7 @@ public class DirectiveDefinition extends AbstractDescribedNode<DirectiveDefiniti
3031

3132
@Internal
3233
protected DirectiveDefinition(String name,
34+
boolean repeatable,
3335
Description description,
3436
List<InputValueDefinition> inputValueDefinitions,
3537
List<DirectiveLocation> directiveLocations,
@@ -39,6 +41,7 @@ protected DirectiveDefinition(String name,
3941
Map<String, String> additionalData) {
4042
super(sourceLocation, comments, ignoredChars, additionalData, description);
4143
this.name = name;
44+
this.repeatable = repeatable;
4245
this.inputValueDefinitions = ImmutableList.copyOf(inputValueDefinitions);
4346
this.directiveLocations = ImmutableList.copyOf(directiveLocations);
4447
}
@@ -49,14 +52,24 @@ protected DirectiveDefinition(String name,
4952
* @param name of the directive definition
5053
*/
5154
public DirectiveDefinition(String name) {
52-
this(name, null, emptyList(), emptyList(), null, emptyList(), IgnoredChars.EMPTY, emptyMap());
55+
this(name, false, null, emptyList(), emptyList(), null, emptyList(), IgnoredChars.EMPTY, emptyMap());
5356
}
5457

5558
@Override
5659
public String getName() {
5760
return name;
5861
}
5962

63+
/**
64+
* An AST node can have multiple directives associated with it IF the directive definition allows
65+
* repeatable directives.
66+
*
67+
* @return true if this directive definition allows repeatable directives
68+
*/
69+
public boolean isRepeatable() {
70+
return repeatable;
71+
}
72+
6073
public List<InputValueDefinition> getInputValueDefinitions() {
6174
return inputValueDefinitions;
6275
}
@@ -106,6 +119,7 @@ public boolean isEqualTo(Node o) {
106119
@Override
107120
public DirectiveDefinition deepCopy() {
108121
return new DirectiveDefinition(name,
122+
repeatable,
109123
description,
110124
deepCopy(inputValueDefinitions),
111125
deepCopy(directiveLocations),
@@ -143,6 +157,7 @@ public static final class Builder implements NodeBuilder {
143157
private SourceLocation sourceLocation;
144158
private List<Comment> comments = new ArrayList<>();
145159
private String name;
160+
private boolean repeatable = false;
146161
private Description description;
147162
private List<InputValueDefinition> inputValueDefinitions = new ArrayList<>();
148163
private List<DirectiveLocation> directiveLocations = new ArrayList<>();
@@ -156,6 +171,7 @@ private Builder(DirectiveDefinition existing) {
156171
this.sourceLocation = existing.getSourceLocation();
157172
this.comments = existing.getComments();
158173
this.name = existing.getName();
174+
this.repeatable = existing.isRepeatable();
159175
this.description = existing.getDescription();
160176
this.inputValueDefinitions = existing.getInputValueDefinitions();
161177
this.directiveLocations = existing.getDirectiveLocations();
@@ -178,6 +194,11 @@ public Builder name(String name) {
178194
return this;
179195
}
180196

197+
public Builder repeatable(boolean repeatable) {
198+
this.repeatable = repeatable;
199+
return this;
200+
}
201+
181202
public Builder description(Description description) {
182203
this.description = description;
183204
return this;
@@ -220,7 +241,7 @@ public Builder additionalData(String key, String value) {
220241

221242

222243
public DirectiveDefinition build() {
223-
return new DirectiveDefinition(name, description, inputValueDefinitions, directiveLocations, sourceLocation, comments, ignoredChars, additionalData);
244+
return new DirectiveDefinition(name, repeatable, description, inputValueDefinitions, directiveLocations, sourceLocation, comments, ignoredChars, additionalData);
224245
}
225246
}
226247
}

0 commit comments

Comments
 (0)