Skip to content

Commit

Permalink
Merge pull request #318 from graphql/directive-lang
Browse files Browse the repository at this point in the history
[RFC] Directives in schema language
  • Loading branch information
leebyron committed Mar 22, 2016
2 parents e89c19d + b633458 commit 345db59
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 345db59

Please sign in to comment.