Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added partial support for repeatable directives #1965

Merged
merged 1 commit into from Jun 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/__fixtures__/schema-kitchen-sink.graphql
Expand Up @@ -142,6 +142,10 @@ directive @include2(if: Boolean!) on
| FRAGMENT_SPREAD
| INLINE_FRAGMENT

directive @myRepeatableDir(name: String!) repeatable on
| OBJECT
| INTERFACE

extend schema @onSchema

extend schema @onSchema {
Expand Down
72 changes: 72 additions & 0 deletions src/language/__tests__/schema-parser-test.js
Expand Up @@ -815,6 +815,78 @@ input Hello {
);
});

it('Directive definition', () => {
const body = 'directive @foo on OBJECT | INTERFACE';
const doc = parse(body);

expect(toJSONDeep(doc)).to.deep.equal({
kind: 'Document',
definitions: [
{
kind: 'DirectiveDefinition',
description: undefined,
name: {
kind: 'Name',
value: 'foo',
loc: { start: 11, end: 14 },
},
arguments: [],
repeatable: false,
locations: [
{
kind: 'Name',
value: 'OBJECT',
loc: { start: 18, end: 24 },
},
{
kind: 'Name',
value: 'INTERFACE',
loc: { start: 27, end: 36 },
},
],
loc: { start: 0, end: 36 },
},
],
loc: { start: 0, end: 36 },
});
});

it('Repeatable directive definition', () => {
const body = 'directive @foo repeatable on OBJECT | INTERFACE';
const doc = parse(body);

expect(toJSONDeep(doc)).to.deep.equal({
kind: 'Document',
definitions: [
{
kind: 'DirectiveDefinition',
description: undefined,
name: {
kind: 'Name',
value: 'foo',
loc: { start: 11, end: 14 },
},
arguments: [],
repeatable: true,
locations: [
{
kind: 'Name',
value: 'OBJECT',
loc: { start: 29, end: 35 },
},
{
kind: 'Name',
value: 'INTERFACE',
loc: { start: 38, end: 47 },
},
],
loc: { start: 0, end: 47 },
},
],
loc: { start: 0, end: 47 },
});
});

it('Directive with incorrect locations', () => {
expectSyntaxError(
`
Expand Down
2 changes: 2 additions & 0 deletions src/language/__tests__/schema-printer-test.js
Expand Up @@ -160,6 +160,8 @@ describe('Printer: SDL document', () => {

directive @include2(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

directive @myRepeatableDir(name: String!) repeatable on OBJECT | INTERFACE

extend schema @onSchema

extend schema @onSchema {
Expand Down
1 change: 1 addition & 0 deletions src/language/ast.js
Expand Up @@ -508,6 +508,7 @@ export type DirectiveDefinitionNode = {
+description?: StringValueNode,
+name: NameNode,
+arguments?: $ReadOnlyArray<InputValueDefinitionNode>,
+repeatable: boolean,
+locations: $ReadOnlyArray<NameNode>,
};

Expand Down
4 changes: 3 additions & 1 deletion src/language/parser.js
Expand Up @@ -1358,7 +1358,7 @@ function parseInputObjectTypeExtension(

/**
* DirectiveDefinition :
* - Description? directive @ Name ArgumentsDefinition? on DirectiveLocations
* - Description? directive @ Name ArgumentsDefinition? `repeatable`? on DirectiveLocations
*/
function parseDirectiveDefinition(lexer: Lexer<*>): DirectiveDefinitionNode {
const start = lexer.token;
Expand All @@ -1367,13 +1367,15 @@ function parseDirectiveDefinition(lexer: Lexer<*>): DirectiveDefinitionNode {
expectToken(lexer, TokenKind.AT);
const name = parseName(lexer);
const args = parseArgumentDefs(lexer);
const repeatable = expectOptionalKeyword(lexer, 'repeatable');
expectKeyword(lexer, 'on');
const locations = parseDirectiveLocations(lexer);
return {
kind: Kind.DIRECTIVE_DEFINITION,
description,
name,
arguments: args,
repeatable,
locations,
loc: loc(lexer, start),
};
Expand Down
3 changes: 2 additions & 1 deletion src/language/printer.js
Expand Up @@ -184,12 +184,13 @@ const printDocASTReducer: any = {
),

DirectiveDefinition: addDescription(
({ name, arguments: args, locations }) =>
({ name, arguments: args, repeatable, locations }) =>
'directive @' +
name +
(hasMultilineItems(args)
? wrap('(\n', indent(join(args, '\n')), '\n)')
: wrap('(', join(args, ', '), ')')) +
(repeatable ? ' repeatable' : '') +
' on ' +
join(locations, ' | '),
),
Expand Down
17 changes: 17 additions & 0 deletions src/type/__tests__/directive-test.js
Expand Up @@ -22,6 +22,7 @@ describe('Type System: Directive', () => {
expect(directive).to.deep.include({
name: 'Foo',
args: [],
isRepeatable: false,
locations: ['QUERY'],
});
});
Expand Down Expand Up @@ -54,6 +55,22 @@ describe('Type System: Directive', () => {
astNode: undefined,
},
],
isRepeatable: false,
locations: ['QUERY'],
});
});

it('defines a repeatable directive', () => {
const directive = new GraphQLDirective({
name: 'Foo',
isRepeatable: true,
locations: ['QUERY'],
});

expect(directive).to.deep.include({
name: 'Foo',
args: [],
isRepeatable: true,
locations: ['QUERY'],
});
});
Expand Down
6 changes: 6 additions & 0 deletions src/type/directives.js
Expand Up @@ -53,13 +53,16 @@ export class GraphQLDirective {
name: string;
description: ?string;
locations: Array<DirectiveLocationEnum>;
isRepeatable: boolean;
args: Array<GraphQLArgument>;
astNode: ?DirectiveDefinitionNode;

constructor(config: GraphQLDirectiveConfig): void {
this.name = config.name;
this.description = config.description;

this.locations = config.locations;
this.isRepeatable = config.isRepeatable != null && config.isRepeatable;
this.astNode = config.astNode;
invariant(config.name, 'Directive must be named.');
invariant(
Expand Down Expand Up @@ -89,12 +92,14 @@ export class GraphQLDirective {
toConfig(): {|
...GraphQLDirectiveConfig,
args: GraphQLFieldConfigArgumentMap,
isRepeatable: boolean,
|} {
return {
name: this.name,
description: this.description,
locations: this.locations,
args: argsToArgsConfig(this.args),
isRepeatable: this.isRepeatable,
astNode: this.astNode,
};
}
Expand All @@ -109,6 +114,7 @@ export type GraphQLDirectiveConfig = {|
description?: ?string,
locations: Array<DirectiveLocationEnum>,
args?: ?GraphQLFieldConfigArgumentMap,
isRepeatable?: ?boolean,
astNode?: ?DirectiveDefinitionNode,
|};

Expand Down
2 changes: 2 additions & 0 deletions src/utilities/__tests__/buildASTSchema-test.js
Expand Up @@ -121,6 +121,8 @@ describe('Schema Builder', () => {
it('With directives', () => {
const sdl = dedent`
directive @foo(arg: Int) on FIELD

directive @repeatableFoo(arg: Int) repeatable on FIELD
`;
expect(cycleSDL(sdl)).to.equal(sdl);
});
Expand Down
5 changes: 3 additions & 2 deletions src/utilities/__tests__/extendSchema-test.js
Expand Up @@ -107,6 +107,7 @@ const FooDirective = new GraphQLDirective({
args: {
input: { type: SomeInputType },
},
isRepeatable: true,
locations: [
DirectiveLocation.SCHEMA,
DirectiveLocation.SCALAR,
Expand Down Expand Up @@ -448,7 +449,7 @@ describe('extendSchema', () => {
interfaceField: String
}

directive @test(arg: Int) on FIELD | SCALAR
directive @test(arg: Int) repeatable on FIELD | SCALAR
`);
const extendedTwiceSchema = extendSchema(extendedSchema, ast);

Expand Down Expand Up @@ -1091,7 +1092,7 @@ describe('extendSchema', () => {

it('may extend directives with new complex directive', () => {
const extendedSchema = extendTestSchema(`
directive @profile(enable: Boolean! tag: String) on QUERY | FIELD
directive @profile(enable: Boolean! tag: String) repeatable on QUERY | FIELD
`);

const extendedDirective = assertDirective(
Expand Down
23 changes: 19 additions & 4 deletions src/utilities/__tests__/schemaPrinter-test.js
Expand Up @@ -461,15 +461,30 @@ describe('Type System Printer', () => {
});

it('Prints custom directives', () => {
const CustomDirective = new GraphQLDirective({
name: 'customDirective',
const SimpleDirective = new GraphQLDirective({
name: 'simpleDirective',
locations: [DirectiveLocation.FIELD],
});
const ComplexDirective = new GraphQLDirective({
name: 'complexDirective',
description: 'Complex Directive',
args: {
stringArg: { type: GraphQLString },
intArg: { type: GraphQLInt, defaultValue: -1 },
},
isRepeatable: true,
locations: [DirectiveLocation.FIELD, DirectiveLocation.QUERY],
});

const Schema = new GraphQLSchema({ directives: [CustomDirective] });
const Schema = new GraphQLSchema({
directives: [SimpleDirective, ComplexDirective],
});
const output = printForTest(Schema);
expect(output).to.equal(dedent`
directive @customDirective on FIELD
directive @simpleDirective on FIELD

"""Complex Directive"""
directive @complexDirective(stringArg: String, intArg: Int = -1) repeatable on FIELD | QUERY
`);
});

Expand Down
1 change: 1 addition & 0 deletions src/utilities/buildASTSchema.js
Expand Up @@ -242,6 +242,7 @@ export class ASTDefinitionBuilder {
name: directive.name.value,
description: getDescription(directive, this._options),
locations,
isRepeatable: directive.repeatable,
args: keyByNameNode(directive.arguments || [], arg => this.buildArg(arg)),
astNode: directive,
});
Expand Down
1 change: 1 addition & 0 deletions src/utilities/schemaPrinter.js
Expand Up @@ -296,6 +296,7 @@ function printDirective(directive, options) {
'directive @' +
directive.name +
printArgs(options, directive.args) +
(directive.isRepeatable ? ' repeatable' : '') +
' on ' +
directive.locations.join(' | ')
);
Expand Down