Skip to content

Commit

Permalink
[RFC] Directives in schema language
Browse files Browse the repository at this point in the history
This adds directives to schema language and to the utilities that use it (schema parser, and buildASTSchema). Directives are one of the few missing pieces from representing a full schema in the schema language.

Note: the schema language is still experimental, so there is no corresponding change to the spec yet.

DirectiveDefinition :
  - directive @ Name ArgumentsDefinition? on DirectiveLocations

DirectiveLocations :
  - Name
  - DirectiveLocations | Name

Example:

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

directive @include(if: Boolean!)
  on FIELD
   | FRAGMENT_SPREAD
   | INLINE_FRAGMENT
```
  • Loading branch information
leebyron committed Mar 22, 2016
1 parent e3f53ec commit b633458
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 19 deletions.
7 changes: 7 additions & 0 deletions src/language/__tests__/schema-kitchen-sink.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ input InputType {
extend type Foo {
seven(argument: [String]): Type
}

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

directive @include(if: Boolean!)
on FIELD
| FRAGMENT_SPREAD
| INLINE_FRAGMENT
5 changes: 5 additions & 0 deletions src/language/__tests__/schema-printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('Printer', () => {

const printed = print(ast);

/* eslint-disable max-len */
expect(printed).to.equal(
`type Foo implements Bar {
one: Type
Expand Down Expand Up @@ -81,6 +82,10 @@ input InputType {
extend type Foo {
seven(argument: [String]): Type
}
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
`);

});
Expand Down
10 changes: 10 additions & 0 deletions src/language/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export type Node = Name
| EnumValueDefinition
| InputObjectTypeDefinition
| TypeExtensionDefinition
| DirectiveDefinition

// Name

Expand All @@ -78,6 +79,7 @@ export type Definition = OperationDefinition
| FragmentDefinition
| TypeDefinition
| TypeExtensionDefinition
| DirectiveDefinition

export type OperationDefinition = {
kind: 'OperationDefinition';
Expand Down Expand Up @@ -332,3 +334,11 @@ export type TypeExtensionDefinition = {
loc?: ?Location;
definition: ObjectTypeDefinition;
}

export type DirectiveDefinition = {
kind: 'DirectiveDefinition';
loc?: ?Location;
name: Name;
arguments?: ?Array<InputValueDefinition>;
locations: Array<Name>;
}
7 changes: 7 additions & 0 deletions src/language/kinds.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,11 @@ export const SCALAR_TYPE_DEFINITION = 'ScalarTypeDefinition';
export const ENUM_TYPE_DEFINITION = 'EnumTypeDefinition';
export const ENUM_VALUE_DEFINITION = 'EnumValueDefinition';
export const INPUT_OBJECT_TYPE_DEFINITION = 'InputObjectTypeDefinition';

// Type Extensions

export const TYPE_EXTENSION_DEFINITION = 'TypeExtensionDefinition';

// Directive Definitions

export const DIRECTIVE_DEFINITION = 'DirectiveDefinition';
38 changes: 38 additions & 0 deletions src/language/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import type {
InputObjectTypeDefinition,

TypeExtensionDefinition,

DirectiveDefinition,
} from './ast';

import {
Expand Down Expand Up @@ -94,6 +96,8 @@ import {
INPUT_OBJECT_TYPE_DEFINITION,

TYPE_EXTENSION_DEFINITION,

DIRECTIVE_DEFINITION,
} from './kinds';


Expand Down Expand Up @@ -205,6 +209,7 @@ function parseDefinition(parser: Parser): Definition {
case 'enum':
case 'input': return parseTypeDefinition(parser);
case 'extend': return parseTypeExtensionDefinition(parser);
case 'directive': return parseDirectiveDefinition(parser);
}
}

Expand Down Expand Up @@ -898,6 +903,39 @@ function parseTypeExtensionDefinition(parser: Parser): TypeExtensionDefinition {
};
}

/**
* DirectiveDefinition :
* - directive @ Name ArgumentsDefinition? on DirectiveLocations
*/
function parseDirectiveDefinition(parser: Parser): DirectiveDefinition {
const start = parser.token.start;
expectKeyword(parser, 'directive');
expect(parser, TokenKind.AT);
const name = parseName(parser);
const args = parseArgumentDefs(parser);
expectKeyword(parser, 'on');
const locations = parseDirectiveLocations(parser);
return {
kind: DIRECTIVE_DEFINITION,
name,
arguments: args,
locations,
loc: loc(parser, start)
};
}

/**
* DirectiveLocations :
* - Name
* - DirectiveLocations | Name
*/
function parseDirectiveLocations(parser: Parser): Array<Name> {
const locations = [];
do {
locations.push(parseName(parser));
} while (skip(parser, TokenKind.PIPE));
return locations;
}

// Core parsing utility functions

Expand Down
4 changes: 4 additions & 0 deletions src/language/printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ const printDocASTReducer = {
`input ${name} ${block(fields)}`,

TypeExtensionDefinition: ({ definition }) => `extend ${definition}`,

DirectiveDefinition: ({ name, arguments: args, locations }) =>
'directive @' + name + wrap('(', join(args, ', '), ')') +
' on ' + join(locations, ' | '),
};

/**
Expand Down
1 change: 1 addition & 0 deletions src/language/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const QueryDocumentKeys = {
EnumValueDefinition: [ 'name' ],
InputObjectTypeDefinition: [ 'name', 'fields' ],
TypeExtensionDefinition: [ 'definition' ],
DirectiveDefinition: [ 'name', 'arguments', 'locations' ],
};

export const BREAK = {};
Expand Down
53 changes: 41 additions & 12 deletions src/type/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/

import { GraphQLNonNull } from './definition';
import type { GraphQLArgument } from './definition';
import { isInputType, GraphQLNonNull } from './definition';
import type {
GraphQLFieldConfigArgumentMap,
GraphQLArgument
} from './definition';
import { GraphQLBoolean } from './scalars';
import invariant from '../jsutils/invariant';
import { assertValidName } from '../utilities/assertValidName';
Expand Down Expand Up @@ -47,15 +50,39 @@ export class GraphQLDirective {
this.name = config.name;
this.description = config.description;
this.locations = config.locations;
this.args = config.args || [];

const args = config.args;
if (!args) {
this.args = [];
} else {
invariant(
!Array.isArray(args),
`@${config.name} args must be an object with argument names as keys.`
);
this.args = Object.keys(args).map(argName => {
assertValidName(argName);
const arg = args[argName];
invariant(
isInputType(arg.type),
`@${config.name}(${argName}:) argument type must be ` +
`Input Type but got: ${arg.type}.`
);
return {
name: argName,
description: arg.description === undefined ? null : arg.description,
type: arg.type,
defaultValue: arg.defaultValue === undefined ? null : arg.defaultValue
};
});
}
}
}

type GraphQLDirectiveConfig = {
name: string;
description?: ?string;
locations: Array<DirectiveLocationEnum>;
args?: ?Array<GraphQLArgument>;
args?: ?GraphQLFieldConfigArgumentMap;
}

/**
Expand All @@ -71,11 +98,12 @@ export const GraphQLIncludeDirective = new GraphQLDirective({
DirectiveLocation.FRAGMENT_SPREAD,
DirectiveLocation.INLINE_FRAGMENT,
],
args: [
{ name: 'if',
args: {
if: {
type: new GraphQLNonNull(GraphQLBoolean),
description: 'Included when true.' }
],
description: 'Included when true.'
}
},
});

/**
Expand All @@ -91,9 +119,10 @@ export const GraphQLSkipDirective = new GraphQLDirective({
DirectiveLocation.FRAGMENT_SPREAD,
DirectiveLocation.INLINE_FRAGMENT,
],
args: [
{ name: 'if',
args: {
if: {
type: new GraphQLNonNull(GraphQLBoolean),
description: 'Skipped when true.' }
],
description: 'Skipped when true.'
}
},
});
12 changes: 12 additions & 0 deletions src/utilities/__tests__/buildASTSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ type HelloScalars {
expect(output).to.equal(body);
});

it('With directives', () => {
const body = `
directive @foo(arg: Int) on FIELD
type Hello {
str: String
}
`;
const output = cycleOutput(body, 'Hello');
expect(output).to.equal(body);
});

it('Type modifiers', () => {
const body = `
type HelloScalars {
Expand Down
4 changes: 4 additions & 0 deletions src/utilities/__tests__/schemaPrinter.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,10 @@ type Root {
const Schema = new GraphQLSchema({ query: Root });
const output = '\n' + printIntrospectionSchema(Schema);
const introspectionSchema = `
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
type __Directive {
name: String!
description: String
Expand Down
20 changes: 20 additions & 0 deletions src/utilities/buildASTSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
UNION_TYPE_DEFINITION,
SCALAR_TYPE_DEFINITION,
INPUT_OBJECT_TYPE_DEFINITION,
DIRECTIVE_DEFINITION,
} from '../language/kinds';

import type {
Expand All @@ -39,6 +40,7 @@ import type {
ScalarTypeDefinition,
EnumTypeDefinition,
InputObjectTypeDefinition,
DirectiveDefinition,
} from '../language/ast';

import {
Expand All @@ -58,6 +60,8 @@ import {
GraphQLNonNull,
} from '../type';

import { GraphQLDirective } from '../type/directives';

import type {
GraphQLType,
GraphQLNamedType
Expand Down Expand Up @@ -115,6 +119,7 @@ export function buildASTSchema(
}

const typeDefs: Array<TypeDefinition> = [];
const directiveDefs: Array<DirectiveDefinition> = [];
for (let i = 0; i < ast.definitions.length; i++) {
const d = ast.definitions[i];
switch (d.kind) {
Expand All @@ -125,6 +130,10 @@ export function buildASTSchema(
case SCALAR_TYPE_DEFINITION:
case INPUT_OBJECT_TYPE_DEFINITION:
typeDefs.push(d);
break;
case DIRECTIVE_DEFINITION:
directiveDefs.push(d);
break;
}
}

Expand Down Expand Up @@ -160,13 +169,24 @@ export function buildASTSchema(

typeDefs.forEach(def => typeDefNamed(def.name.value));

const directives = directiveDefs.map(getDirective);

return new GraphQLSchema({
directives,
query: getObjectType(astMap[queryTypeName]),
mutation: mutationTypeName ? getObjectType(astMap[mutationTypeName]) : null,
subscription:
subscriptionTypeName ? getObjectType(astMap[subscriptionTypeName]) : null,
});

function getDirective(directiveAST: DirectiveDefinition): GraphQLDirective {
return new GraphQLDirective({
name: directiveAST.name.value,
locations: directiveAST.locations.map(node => node.value),
args: makeInputValues(directiveAST.arguments),
});
}

function getObjectType(typeAST: TypeDefinition): GraphQLObjectType {
const type = typeDefNamed(typeAST.name.value);
invariant(
Expand Down
2 changes: 1 addition & 1 deletion src/utilities/buildClientSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ export function buildClientSchema(
name: directiveIntrospection.name,
description: directiveIntrospection.description,
locations,
args: directiveIntrospection.args.map(buildInputValue),
args: buildInputValueDefMap(directiveIntrospection.args),
});
}

Expand Down
Loading

0 comments on commit b633458

Please sign in to comment.