From d2b66ab161fae837e0998d7785cd0fa7a17d27c8 Mon Sep 17 00:00:00 2001 From: dugenkui Date: Fri, 22 May 2020 01:39:19 +0800 Subject: [PATCH] add repeatable directives --- src/main/antlr/GraphqlCommon.g4 | 1 + src/main/antlr/GraphqlSDL.g4 | 2 +- .../graphql/introspection/Introspection.java | 7 +- .../graphql/language/DirectiveDefinition.java | 20 ++++- .../parser/GraphqlAntlrToLanguage.java | 1 + .../UniqueDirectiveNamesPerLocation.java | 21 +++-- .../RepeatableDirectivesTest.groovy | 87 +++++++++++++++++++ 7 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 src/test/groovy/graphql/execution/directives/RepeatableDirectivesTest.groovy diff --git a/src/main/antlr/GraphqlCommon.g4 b/src/main/antlr/GraphqlCommon.g4 index ed7f579128..9887cc15c2 100644 --- a/src/main/antlr/GraphqlCommon.g4 +++ b/src/main/antlr/GraphqlCommon.g4 @@ -91,6 +91,7 @@ INPUT: 'input'; EXTEND: 'extend'; DIRECTIVE: 'directive'; ON_KEYWORD: 'on'; +REPEATABLE: 'repeatable'; NAME: [_A-Za-z][_0-9A-Za-z]*; diff --git a/src/main/antlr/GraphqlSDL.g4 b/src/main/antlr/GraphqlSDL.g4 index fd0bf349ac..ccca53ee49 100644 --- a/src/main/antlr/GraphqlSDL.g4 +++ b/src/main/antlr/GraphqlSDL.g4 @@ -120,7 +120,7 @@ inputObjectValueDefinitions : '{' inputValueDefinition* '}'; extensionInputObjectValueDefinitions : '{' inputValueDefinition+ '}'; -directiveDefinition : description? DIRECTIVE '@' name argumentsDefinition? 'on' directiveLocations; +directiveDefinition : description? DIRECTIVE '@' name argumentsDefinition? REPEATABLE? ON_KEYWORD directiveLocations; directiveLocation : name; diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index 39ad2c93a9..9b8d680f9e 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -418,16 +418,19 @@ public enum DirectiveLocation { .name("__Directive") .field(newFieldDefinition() .name("name") - .type(GraphQLString)) + .type(nonNull(GraphQLString))) .field(newFieldDefinition() .name("description") .type(GraphQLString)) .field(newFieldDefinition() .name("locations") - .type(list(nonNull(__DirectiveLocation)))) + .type(nonNull(list(nonNull(__DirectiveLocation))))) .field(newFieldDefinition() .name("args") .type(nonNull(list(nonNull(__InputValue))))) + .field(newFieldDefinition() + .name("isRepeatable") + .type(nonNull(GraphQLBoolean))) .field(newFieldDefinition() .name("onOperation") .type(GraphQLBoolean) diff --git a/src/main/java/graphql/language/DirectiveDefinition.java b/src/main/java/graphql/language/DirectiveDefinition.java index cb5a977be9..bd9af87e43 100644 --- a/src/main/java/graphql/language/DirectiveDefinition.java +++ b/src/main/java/graphql/language/DirectiveDefinition.java @@ -21,6 +21,7 @@ public class DirectiveDefinition extends AbstractDescribedNode inputValueDefinitions; private final List directiveLocations; + private final boolean isRepeatable; public static final String CHILD_INPUT_VALUE_DEFINITIONS = "inputValueDefinitions"; public static final String CHILD_DIRECTIVE_LOCATION = "directiveLocation"; @@ -30,6 +31,7 @@ protected DirectiveDefinition(String name, Description description, List inputValueDefinitions, List directiveLocations, + boolean isRepeatable, SourceLocation sourceLocation, List comments, IgnoredChars ignoredChars, @@ -38,6 +40,7 @@ protected DirectiveDefinition(String name, this.name = name; this.inputValueDefinitions = inputValueDefinitions; this.directiveLocations = directiveLocations; + this.isRepeatable = isRepeatable; } /** @@ -46,7 +49,7 @@ protected DirectiveDefinition(String name, * @param name of the directive definition */ public DirectiveDefinition(String name) { - this(name, null, new ArrayList<>(), new ArrayList<>(), null, new ArrayList<>(), IgnoredChars.EMPTY, emptyMap()); + this(name, null, new ArrayList<>(), new ArrayList<>(), false, null, new ArrayList<>(), IgnoredChars.EMPTY, emptyMap()); } @Override @@ -62,6 +65,10 @@ public List getDirectiveLocations() { return new ArrayList<>(directiveLocations); } + public boolean isRepeatable() { + return isRepeatable; + } + @Override public List getChildren() { List result = new ArrayList<>(); @@ -106,6 +113,7 @@ public DirectiveDefinition deepCopy() { description, deepCopy(inputValueDefinitions), deepCopy(directiveLocations), + isRepeatable, getSourceLocation(), getComments(), getIgnoredChars(), @@ -118,6 +126,7 @@ public String toString() { "name='" + name + "'" + ", inputValueDefinitions=" + inputValueDefinitions + ", directiveLocations=" + directiveLocations + + ", isRepeatable=" + isRepeatable + "}"; } @@ -143,6 +152,7 @@ public static final class Builder implements NodeBuilder { private Description description; private List inputValueDefinitions = new ArrayList<>(); private List directiveLocations = new ArrayList<>(); + private boolean isRepeatable; private IgnoredChars ignoredChars = IgnoredChars.EMPTY; private Map additionalData = new LinkedHashMap<>(); @@ -156,6 +166,7 @@ private Builder(DirectiveDefinition existing) { this.description = existing.getDescription(); this.inputValueDefinitions = existing.getInputValueDefinitions(); this.directiveLocations = existing.getDirectiveLocations(); + this.isRepeatable = existing.isRepeatable(); this.ignoredChars = existing.getIgnoredChars(); this.additionalData = new LinkedHashMap<>(existing.getAdditionalData()); } @@ -200,6 +211,11 @@ public Builder directiveLocation(DirectiveLocation directiveLocation) { return this; } + public Builder isRepeatable(boolean isRepeatable) { + this.isRepeatable = isRepeatable; + return this; + } + public Builder ignoredChars(IgnoredChars ignoredChars) { this.ignoredChars = ignoredChars; return this; @@ -217,7 +233,7 @@ public Builder additionalData(String key, String value) { public DirectiveDefinition build() { - return new DirectiveDefinition(name, description, inputValueDefinitions, directiveLocations, sourceLocation, comments, ignoredChars, additionalData); + return new DirectiveDefinition(name, description, inputValueDefinitions, directiveLocations, isRepeatable, sourceLocation, comments, ignoredChars, additionalData); } } } diff --git a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java index 772a7fb272..a293e874c0 100644 --- a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java +++ b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java @@ -637,6 +637,7 @@ protected DirectiveDefinition createDirectiveDefinition(GraphqlParser.DirectiveD if (ctx.argumentsDefinition() != null) { def.inputValueDefinitions(createInputValueDefinitions(ctx.argumentsDefinition().inputValueDefinition())); } + def.isRepeatable(ctx.REPEATABLE() != null); return def.build(); } diff --git a/src/main/java/graphql/validation/rules/UniqueDirectiveNamesPerLocation.java b/src/main/java/graphql/validation/rules/UniqueDirectiveNamesPerLocation.java index 800ce9452a..03a432a261 100644 --- a/src/main/java/graphql/validation/rules/UniqueDirectiveNamesPerLocation.java +++ b/src/main/java/graphql/validation/rules/UniqueDirectiveNamesPerLocation.java @@ -8,6 +8,7 @@ import graphql.language.InlineFragment; import graphql.language.Node; import graphql.language.OperationDefinition; +import graphql.schema.GraphQLDirective; import graphql.validation.AbstractRule; import graphql.validation.ValidationContext; import graphql.validation.ValidationErrorCollector; @@ -57,17 +58,23 @@ public void checkOperationDefinition(OperationDefinition operationDefinition) { } private void checkDirectivesUniqueness(Node directivesContainer, List directives) { - Set names = new LinkedHashSet<>(); - directives.forEach(directive -> { - String name = directive.getName(); - if (names.contains(name)) { + Set directiveNames = new LinkedHashSet<>(); + + for (Directive directive : directives) { + String directiveName = directive.getName(); + GraphQLDirective graphQLDirective = getValidationContext().getSchema().getDirective(directiveName); + + if (graphQLDirective == null) continue; + if (graphQLDirective.getDefinition() != null && graphQLDirective.getDefinition().isRepeatable()) continue; + + if (directiveNames.contains(directiveName)) { addError(ValidationErrorType.DuplicateDirectiveName, directive.getSourceLocation(), - duplicateDirectiveNameMessage(name, directivesContainer.getClass().getSimpleName())); + duplicateDirectiveNameMessage(directiveName, directivesContainer.getClass().getSimpleName())); } else { - names.add(name); + directiveNames.add(directiveName); } - }); + } } private String duplicateDirectiveNameMessage(String directiveName, String location) { diff --git a/src/test/groovy/graphql/execution/directives/RepeatableDirectivesTest.groovy b/src/test/groovy/graphql/execution/directives/RepeatableDirectivesTest.groovy new file mode 100644 index 0000000000..89fce64c8c --- /dev/null +++ b/src/test/groovy/graphql/execution/directives/RepeatableDirectivesTest.groovy @@ -0,0 +1,87 @@ +package graphql.execution.directives + +import graphql.TestUtil +import graphql.language.Directive +import graphql.language.Field +import graphql.language.OperationDefinition +import graphql.language.StringValue +import graphql.validation.Validator +import spock.lang.Specification + +class RepeatableDirectivesTest extends Specification { + + def sdl = ''' + directive @repeatableDirective(arg: String) repeatable on FIELD + + directive @nonRepeatableDirective on FIELD + + type Query { + namedField: String + } + ''' + + def schema = TestUtil.schema(sdl) + + + def "repeatableDirectives"() { + def spec = ''' + query { + f1: namedField @repeatableDirective @repeatableDirective + f2: namedField @repeatableDirective + f3: namedField @nonRepeatableDirective + } + ''' + + when: + def document = TestUtil.parseQuery(spec) + def validator = new Validator(); + def validationErrors = validator.validateDocument(schema, document); + + then: + validationErrors.size() == 0 + } + + def "nonRepeatableDirective"() { + + def spec = ''' + query { + namedField @nonRepeatableDirective @nonRepeatableDirective + } + ''' + + when: + def document = TestUtil.parseQuery(spec) + def validator = new Validator(); + def validationErrors = validator.validateDocument(schema, document); + + then: + validationErrors.size() == 1 + validationErrors[0].message == "Validation error of type DuplicateDirectiveName: Directives must be uniquely named within a location. The directive 'nonRepeatableDirective' used on a 'Field' is not unique. @ 'namedField'" + } + + def "getRepeatableDirectivesInfo"() { + + def spec = ''' + query { + namedField @repeatableDirective(arg: "value1") @repeatableDirective(arg: "value2") + } + ''' + + when: + def document = TestUtil.parseQuery(spec) + def validator = new Validator(); + def validationErrors = validator.validateDocument(schema, document); + + OperationDefinition operationDefinition = document.getDefinitions()[0] + Field field = operationDefinition.getSelectionSet().getSelections()[0] + List directives = field.getDirectives() + + then: + validationErrors.size() == 0 + directives.size() == 2 + ((StringValue) directives[0].getArgument("arg").getValue()).getValue() == "value1" + ((StringValue) directives[1].getArgument("arg").getValue()).getValue() == "value2" + } + + +}