Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/antlr/GraphqlCommon.g4
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ INPUT: 'input';
EXTEND: 'extend';
DIRECTIVE: 'directive';
ON_KEYWORD: 'on';
REPEATABLE: 'repeatable';
NAME: [_A-Za-z][_0-9A-Za-z]*;


Expand Down
2 changes: 1 addition & 1 deletion src/main/antlr/GraphqlSDL.g4
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
7 changes: 5 additions & 2 deletions src/main/java/graphql/introspection/Introspection.java
Original file line number Diff line number Diff line change
Expand Up @@ -418,16 +418,19 @@ public enum DirectiveLocation {
.name("__Directive")
.field(newFieldDefinition()
.name("name")
.type(GraphQLString))
.type(nonNull(GraphQLString)))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch

.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)
Expand Down
20 changes: 18 additions & 2 deletions src/main/java/graphql/language/DirectiveDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class DirectiveDefinition extends AbstractDescribedNode<DirectiveDefiniti
private final String name;
private final List<InputValueDefinition> inputValueDefinitions;
private final List<DirectiveLocation> directiveLocations;
private final boolean isRepeatable;

public static final String CHILD_INPUT_VALUE_DEFINITIONS = "inputValueDefinitions";
public static final String CHILD_DIRECTIVE_LOCATION = "directiveLocation";
Expand All @@ -30,6 +31,7 @@ protected DirectiveDefinition(String name,
Description description,
List<InputValueDefinition> inputValueDefinitions,
List<DirectiveLocation> directiveLocations,
boolean isRepeatable,
SourceLocation sourceLocation,
List<Comment> comments,
IgnoredChars ignoredChars,
Expand All @@ -38,6 +40,7 @@ protected DirectiveDefinition(String name,
this.name = name;
this.inputValueDefinitions = inputValueDefinitions;
this.directiveLocations = directiveLocations;
this.isRepeatable = isRepeatable;
}

/**
Expand All @@ -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
Expand All @@ -62,6 +65,10 @@ public List<DirectiveLocation> getDirectiveLocations() {
return new ArrayList<>(directiveLocations);
}

public boolean isRepeatable() {
return isRepeatable;
}

@Override
public List<Node> getChildren() {
List<Node> result = new ArrayList<>();
Expand Down Expand Up @@ -106,6 +113,7 @@ public DirectiveDefinition deepCopy() {
description,
deepCopy(inputValueDefinitions),
deepCopy(directiveLocations),
isRepeatable,
getSourceLocation(),
getComments(),
getIgnoredChars(),
Expand All @@ -118,6 +126,7 @@ public String toString() {
"name='" + name + "'" +
", inputValueDefinitions=" + inputValueDefinitions +
", directiveLocations=" + directiveLocations +
", isRepeatable=" + isRepeatable +
"}";
}

Expand All @@ -143,6 +152,7 @@ public static final class Builder implements NodeBuilder {
private Description description;
private List<InputValueDefinition> inputValueDefinitions = new ArrayList<>();
private List<DirectiveLocation> directiveLocations = new ArrayList<>();
private boolean isRepeatable;
private IgnoredChars ignoredChars = IgnoredChars.EMPTY;
private Map<String, String> additionalData = new LinkedHashMap<>();

Expand All @@ -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());
}
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
}
}
1 change: 1 addition & 0 deletions src/main/java/graphql/parser/GraphqlAntlrToLanguage.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,17 +58,23 @@ public void checkOperationDefinition(OperationDefinition operationDefinition) {
}

private void checkDirectivesUniqueness(Node<?> directivesContainer, List<Directive> directives) {
Set<String> names = new LinkedHashSet<>();
directives.forEach(directive -> {
String name = directive.getName();
if (names.contains(name)) {
Set<String> directiveNames = new LinkedHashSet<>();

for (Directive directive : directives) {
String directiveName = directive.getName();
GraphQLDirective graphQLDirective = getValidationContext().getSchema().getDirective(directiveName);

if (graphQLDirective == null) continue;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use fully formed if statements

if (graphQLDirective == null) {
    continue;
}

more lines : yes - more readable : also yes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by using the GraphQL java code formatter it will be automatically handled.

if (graphQLDirective.getDefinition() != null && graphQLDirective.getDefinition().isRepeatable()) continue;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will want to transfer the AST isRepeatable into the runtime GraphQLDirective class.

That is inside SchemaGenerator related code we transfer these booleans over

The AST elements are for reading SDL - the GraphqlXXX class are aimed as the runtime equivalents with more logic in them and more state rules.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for your guidance


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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Directive> 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"
}


}