Skip to content

Commit

Permalink
Added partial support for repeatable directives (#1965)
Browse files Browse the repository at this point in the history
Code is based on #1541 but without introspection changes
and without breaking change detection
  • Loading branch information
IvanGoncharov committed Jun 9, 2019
1 parent 24fa31a commit 2c7224c
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 47 deletions.
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

0 comments on commit 2c7224c

Please sign in to comment.