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

[RFC] Directives in schema language #318

Merged
merged 2 commits into from
Mar 22, 2016
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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