diff --git a/src/language/parser.js b/src/language/parser.js index 17451bdc83..4b11f2d07c 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -9,8 +9,7 @@ import { Source } from './source'; import { syntaxError } from '../error'; -import { lex, TokenKind, getTokenKindDesc, getTokenDesc } from './lexer'; -import type { Token } from './lexer'; +import { TokenKind } from './lexer'; import type { Name, Variable, @@ -35,7 +34,7 @@ import type { Directive, Type, - NamedType + NamedType, } from './ast'; import { @@ -69,24 +68,19 @@ import { NON_NULL_TYPE, } from './kinds'; -/** - * Configuration options to control parser behavior - */ -type ParseOptions = { - /** - * By default, the parser creates AST nodes that know the location - * in the source that they correspond to. This configuration flag - * disables that behavior for performance or testing. - */ - noLocation?: boolean, - - /** - * By default, the parser creates AST nodes that contain a reference - * to the source that they were created from. This configuration flag - * disables that behavior for performance or testing. - */ - noSource?: boolean, -} +import { + makeParser, + peek, + skip, + loc, + any, + many, + expect, + unexpected, + expectKeyword, + advance, + ParseOptions, +} from './parserCore'; /** * Given a GraphQL source, parses it into a Document. @@ -101,162 +95,10 @@ export function parse( return parseDocument(parser); } -/** - * Returns the parser object that is used to store state throughout the - * process of parsing. - */ -function makeParser(source: Source, options: ParseOptions) { - var _lexToken = lex(source); - return { - _lexToken, - source, - options, - prevEnd: 0, - token: _lexToken(), - }; -} - -/** - * Returns a location object, used to identify the place in - * the source that created a given parsed object. - */ -function loc(parser, start: number) { - if (parser.options.noLocation) { - return null; - } - if (parser.options.noSource) { - return { - start: start, - end: parser.prevEnd - }; - } - return { - start: start, - end: parser.prevEnd, - source: parser.source - }; -} - -/** - * Moves the internal parser object to the next lexed token. - */ -function advance(parser): void { - var prevEnd = parser.token.end; - parser.prevEnd = prevEnd; - parser.token = parser._lexToken(prevEnd); -} - -/** - * Determines if the next token is of a given kind - */ -function peek(parser, kind: string): boolean { - return parser.token.kind === kind; -} - -/** - * If the next token is of the given kind, return true after advancing - * the parser. Otherwise, do not change the parser state and return false. - */ -function skip(parser, kind: string): boolean { - var match = parser.token.kind === kind; - if (match) { - advance(parser); - } - return match; -} - -/** - * If the next token is of the given kind, return that token after advancing - * the parser. Otherwise, do not change the parser state and return false. - */ -function expect(parser, kind: string): Token { - var token = parser.token; - if (token.kind === kind) { - advance(parser); - return token; - } - throw syntaxError( - parser.source, - token.start, - `Expected ${getTokenKindDesc(kind)}, found ${getTokenDesc(token)}` - ); -} - -/** - * If the next token is a keyword with the given value, return that token after - * advancing the parser. Otherwise, do not change the parser state and return - * false. - */ -function expectKeyword(parser, value: string): Token { - var token = parser.token; - if (token.kind === TokenKind.NAME && token.value === value) { - advance(parser); - return token; - } - throw syntaxError( - parser.source, - token.start, - `Expected "${value}", found ${getTokenDesc(token)}` - ); -} - -/** - * Helper function for creating an error when an unexpected lexed token - * is encountered. - */ -function unexpected(parser, atToken?: ?Token): Error { - var token = atToken || parser.token; - return syntaxError( - parser.source, - token.start, - `Unexpected ${getTokenDesc(token)}` - ); -} - -/** - * Returns a possibly empty list of parse nodes, determined by - * the parseFn. This list begins with a lex token of openKind - * and ends with a lex token of closeKind. Advances the parser - * to the next lex token after the closing token. - */ -function any( - parser, - openKind: number, - parseFn: (parser: any) => T, - closeKind: number -): Array { - expect(parser, openKind); - var nodes = []; - while (!skip(parser, closeKind)) { - nodes.push(parseFn(parser)); - } - return nodes; -} - -/** - * Returns a non-empty list of parse nodes, determined by - * the parseFn. This list begins with a lex token of openKind - * and ends with a lex token of closeKind. Advances the parser - * to the next lex token after the closing token. - */ -function many( - parser, - openKind: number, - parseFn: (parser: any) => T, - closeKind: number -): Array { - expect(parser, openKind); - var nodes = [parseFn(parser)]; - while (!skip(parser, closeKind)) { - nodes.push(parseFn(parser)); - } - return nodes; -} - /** * Converts a name lex token into a name parse node. */ -function parseName(parser): Name { +export function parseName(parser): Name { var token = expect(parser, TokenKind.NAME); return { kind: NAME, @@ -265,7 +107,6 @@ function parseName(parser): Name { }; } - // Implements the parsing rules in the Document section. function parseDocument(parser): Document { @@ -603,7 +444,7 @@ function parseDirective(parser): Directive { /** * Handles the Type: NamedType, ListType, and NonNullType parsing rules. */ -function parseType(parser): Type { +export function parseType(parser): Type { var start = parser.token.start; var type; if (skip(parser, TokenKind.BRACKET_L)) { @@ -627,7 +468,7 @@ function parseType(parser): Type { return type; } -function parseNamedType(parser): NamedType { +export function parseNamedType(parser): NamedType { var start = parser.token.start; return { kind: NAMED_TYPE, diff --git a/src/language/parserCore.js b/src/language/parserCore.js new file mode 100644 index 0000000000..f748c0c05a --- /dev/null +++ b/src/language/parserCore.js @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import { lex, TokenKind, getTokenKindDesc, getTokenDesc } from './lexer'; + +import { Source } from './source'; +import { syntaxError } from '../error'; +import type { Token } from './lexer'; + +/** + * Returns the parser object that is used to store state throughout the + * process of parsing. + */ +export function makeParser(source: Source, options: ParseOptions) { + var _lexToken = lex(source); + return { + _lexToken, + source, + options, + prevEnd: 0, + token: _lexToken(), + }; +} + +/** + * Configuration options to control parser behavior + */ +export type ParseOptions = { + /** + * By default, the parser creates AST nodes that know the location + * in the source that they correspond to. This configuration flag + * disables that behavior for performance or testing. + */ + noLocation?: boolean, + + /** + * By default, the parser creates AST nodes that contain a reference + * to the source that they were created from. This configuration flag + * disables that behavior for performance or testing. + */ + noSource?: boolean, +} + +/** + * Returns a location object, used to identify the place in + * the source that created a given parsed object. + */ +export function loc(parser, start: number) { + if (parser.options.noLocation) { + return null; + } + if (parser.options.noSource) { + return { + start: start, + end: parser.prevEnd + }; + } + return { + start: start, + end: parser.prevEnd, + source: parser.source + }; +} + +/** + * Moves the internal parser object to the next lexed token. + */ +export function advance(parser): void { + var prevEnd = parser.token.end; + parser.prevEnd = prevEnd; + parser.token = parser._lexToken(prevEnd); +} + +/** + * Determines if the next token is of a given kind + */ +export function peek(parser, kind: string): boolean { + return parser.token.kind === kind; +} + +/** + * If the next token is of the given kind, return true after advancing + * the parser. Otherwise, do not change the parser state and return false. + */ +export function skip(parser, kind: string): boolean { + var match = parser.token.kind === kind; + if (match) { + advance(parser); + } + return match; +} + +/** + * If the next token is of the given kind, return that token after advancing + * the parser. Otherwise, do not change the parser state and return false. + */ +export function expect(parser, kind: string): Token { + var token = parser.token; + if (token.kind === kind) { + advance(parser); + return token; + } + throw syntaxError( + parser.source, + token.start, + `Expected ${getTokenKindDesc(kind)}, found ${getTokenDesc(token)}` + ); +} + +/** + * If the next token is a keyword with the given value, return that token after + * advancing the parser. Otherwise, do not change the parser state and return + * false. + */ +export function expectKeyword(parser, value: string): Token { + var token = parser.token; + if (token.kind === TokenKind.NAME && token.value === value) { + advance(parser); + return token; + } + throw syntaxError( + parser.source, + token.start, + `Expected "${value}", found ${getTokenDesc(token)}` + ); +} + +/** + * Helper export function for creating an error when an unexpected lexed token + * is encountered. + */ +export function unexpected(parser, atToken?: ?Token): Error { + var token = atToken || parser.token; + return syntaxError( + parser.source, + token.start, + `Unexpected ${getTokenDesc(token)}` + ); +} + +/** + * Returns a possibly empty list of parse nodes, determined by + * the parseFn. This list begins with a lex token of openKind + * and ends with a lex token of closeKind. Advances the parser + * to the next lex token after the closing token. + */ +export function any( + parser, + openKind: number, + parseFn: (parser: any) => T, + closeKind: number +): Array { + expect(parser, openKind); + var nodes = []; + while (!skip(parser, closeKind)) { + nodes.push(parseFn(parser)); + } + return nodes; +} + +/** + * Returns a non-empty list of parse nodes, determined by + * the parseFn. This list begins with a lex token of openKind + * and ends with a lex token of closeKind. Advances the parser + * to the next lex token after the closing token. + */ +export function many( + parser, + openKind: number, + parseFn: (parser: any) => T, + closeKind: number +): Array { + expect(parser, openKind); + var nodes = [parseFn(parser)]; + while (!skip(parser, closeKind)) { + nodes.push(parseFn(parser)); + } + return nodes; +} diff --git a/src/language/schema/__tests__/parser.js b/src/language/schema/__tests__/parser.js new file mode 100644 index 0000000000..d4917e50a8 --- /dev/null +++ b/src/language/schema/__tests__/parser.js @@ -0,0 +1,477 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { parseSchema } from '../parser'; + +function createLocFn(body) { + return (start, end) => ({ + start: start, + end: end, + source: { + body: body, + name: 'GraphQL', + }, + }); +} + +function printJson(obj) { + return JSON.stringify(obj, null, 2); +} + +function typeNode(name, loc) { + return { + kind: 'NamedType', + name: nameNode(name, loc), + loc: loc, + }; +} + +function nameNode(name, loc) { + return { + kind: 'Name', + value: name, + loc: loc, + }; +} + +function fieldNode(name, type, loc) { + return fieldNodeWithArgs(name, type, [], loc); +} + +function fieldNodeWithArgs(name, type, args, loc) { + return { + kind: 'FieldDefinition', + name: name, + type: type, + arguments: args, + loc: loc, + }; +} + +function inputFieldNode(name, type, loc) { + return { + kind: 'InputFieldDefinition', + name: name, + type: type, + loc: loc, + }; +} + +describe('Schema Parser', () => { + it('Simple type', () => { + var body = ` +type Hello { + world: String +}`; + var doc = parseSchema(body); + var loc = createLocFn(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'TypeDefinition', + name: nameNode('Hello', loc(6, 11)), + interfaces: [], + fields: [ + fieldNode( + nameNode('world', loc(16, 21)), + typeNode('String', loc(23, 29)), + loc(16, 29) + ) + ], + loc: loc(1, 31), + } + ], + loc: loc(1, 31), + }; + expect(printJson(expected)).to.equal(printJson(doc)); + }); + + it('Simple non-null type', () => { + var body = ` +type Hello { + world: String! +}`; + var loc = createLocFn(body); + var doc = parseSchema(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'TypeDefinition', + name: nameNode('Hello', loc(6, 11)), + interfaces: [], + fields: [ + fieldNode( + nameNode('world', loc(16, 21)), + { + kind: 'NonNullType', + type: typeNode('String', loc(23, 29)), + loc: loc(23, 30), + }, + loc(16, 30) + ) + ], + loc: loc(1, 32), + } + ], + loc: loc(1, 32), + }; + expect(printJson(expected)).to.equal(printJson(doc)); + }); + + + it('Simple type inheriting interface', () => { + var body = `type Hello implements World { }`; + var loc = createLocFn(body); + var doc = parseSchema(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'TypeDefinition', + name: nameNode('Hello', loc(5, 10)), + interfaces: [typeNode('World', loc(22, 27))], + fields: [], + loc: loc(0, 31), + } + ], + loc: loc(0, 31), + }; + expect(printJson(doc)).to.equal(printJson(expected)); + }); + + it('Simple type inheriting multiple interfaces', () => { + var body = `type Hello implements Wo, rld { }`; + var loc = createLocFn(body); + var doc = parseSchema(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'TypeDefinition', + name: nameNode('Hello', loc(5, 10)), + interfaces: [ + typeNode('Wo', loc(22, 24)), + typeNode('rld', loc(26, 29)) + ], + fields: [], + loc: loc(0, 33), + } + ], + loc: loc(0, 33), + }; + expect(printJson(doc)).to.equal(printJson(expected)); + }); + + function enumValueNode(name, loc) { + return { + kind: 'EnumValueDefinition', + name: nameNode(name, loc), + loc: loc, + }; + } + + it('Single value enum', () => { + var body = `enum Hello { WORLD }`; + var loc = createLocFn(body); + var doc = parseSchema(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'EnumDefinition', + name: nameNode('Hello', loc(5, 10)), + values: [enumValueNode('WORLD', loc(13, 18))], + loc: loc(0, 20), + } + ], + loc: loc(0, 20), + }; + expect(printJson(doc)).to.equal(printJson(expected)); + }); + + it('Double value enum', () => { + var body = `enum Hello { WO, RLD }`; + var loc = createLocFn(body); + var doc = parseSchema(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'EnumDefinition', + name: nameNode('Hello', loc(5, 10)), + values: [ + enumValueNode('WO', loc(13, 15)), + enumValueNode('RLD', loc(17, 20)), + ], + loc: loc(0, 22), + } + ], + loc: loc(0, 22), + }; + expect(printJson(doc)).to.equal(printJson(expected)); + }); + + it('Simple interface', () => { + var body = ` +interface Hello { + world: String +}`; + var doc = parseSchema(body); + var loc = createLocFn(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'InterfaceDefinition', + name: nameNode('Hello', loc(11, 16)), + fields: [ + fieldNode( + nameNode('world', loc(21, 26)), + typeNode('String', loc(28, 34)), + loc(21, 34) + ) + ], + loc: loc(1, 36), + } + ], + loc: loc(1, 36), + }; + expect(printJson(expected)).to.equal(printJson(doc)); + }); + + it('Simple field with arg', () => { + var body = ` +type Hello { + world(flag: Boolean): String +}`; + var doc = parseSchema(body); + var loc = createLocFn(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'TypeDefinition', + name: nameNode('Hello', loc(6, 11)), + interfaces: [], + fields: [ + fieldNodeWithArgs( + nameNode('world', loc(16, 21)), + typeNode('String', loc(38, 44)), + [ + { + kind: 'ArgumentDefinition', + name: nameNode('flag', loc(22, 26)), + type: typeNode('Boolean', loc(28, 35)), + loc: loc(22, 35), + } + ], + loc(16, 44) + ) + ], + loc: loc(1, 46), + } + ], + loc: loc(1, 46), + }; + expect(printJson(expected)).to.equal(printJson(doc)); + }); + + it('Simple field with list arg', () => { + var body = ` +type Hello { + world(things: [String]): String +}`; + var doc = parseSchema(body); + var loc = createLocFn(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'TypeDefinition', + name: nameNode('Hello', loc(6, 11)), + interfaces: [], + fields: [ + fieldNodeWithArgs( + nameNode('world', loc(16, 21)), + typeNode('String', loc(41, 47)), + [ + { + kind: 'ArgumentDefinition', + name: nameNode('things', loc(22, 28)), + type: { + kind: 'ListType', + type: typeNode('String', loc(31, 37)), + loc: loc(30, 38) + }, + loc: loc(22, 38), + } + ], + loc(16, 47) + ) + ], + loc: loc(1, 49), + } + ], + loc: loc(1, 49), + }; + expect(printJson(expected)).to.equal(printJson(doc)); + }); + + it('Simple field with two args', () => { + var body = ` +type Hello { + world(argOne: Boolean, argTwo: Int): String +}`; + var doc = parseSchema(body); + var loc = createLocFn(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'TypeDefinition', + name: nameNode('Hello', loc(6, 11)), + interfaces: [], + fields: [ + fieldNodeWithArgs( + nameNode('world', loc(16, 21)), + typeNode('String', loc(53, 59)), + [ + { + kind: 'ArgumentDefinition', + name: nameNode('argOne', loc(22, 28)), + type: typeNode('Boolean', loc(30, 37)), + loc: loc(22, 37), + }, + { + kind: 'ArgumentDefinition', + name: nameNode('argTwo', loc(39, 45)), + type: typeNode('Int', loc(47, 50)), + loc: loc(39, 50), + }, + ], + loc(16, 59) + ) + ], + loc: loc(1, 61), + } + ], + loc: loc(1, 61), + }; + expect(printJson(expected)).to.equal(printJson(doc)); + }); + + it('Simple union', () => { + var body = `union Hello { World }`; + var doc = parseSchema(body); + var loc = createLocFn(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'UnionDefinition', + name: nameNode('Hello', loc(6, 11)), + types: [typeNode('World', loc(14, 19))], + loc: loc(0, 21), + } + ], + loc: loc(0, 21), + }; + expect(printJson(expected)).to.equal(printJson(doc)); + }); + + it('Union with two types', () => { + var body = `union Hello { Wo | Rld }`; + var doc = parseSchema(body); + var loc = createLocFn(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'UnionDefinition', + name: nameNode('Hello', loc(6, 11)), + types: [ + typeNode('Wo', loc(14, 16)), + typeNode('Rld', loc(19, 22)), + ], + loc: loc(0, 24), + } + ], + loc: loc(0, 24), + }; + expect(printJson(expected)).to.equal(printJson(doc)); + }); + + it('Scalar', () => { + var body = `scalar Hello`; + var doc = parseSchema(body); + var loc = createLocFn(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'ScalarDefinition', + name: nameNode('Hello', loc(7, 12)), + loc: loc(0, 12), + } + ], + loc: loc(0, 12), + }; + expect(printJson(expected)).to.equal(printJson(doc)); + }); + + it('Simple input object', () => { + var body = ` +input Hello { + world: String +}`; + var doc = parseSchema(body); + var loc = createLocFn(body); + var expected = { + kind: 'SchemaDocument', + definitions: [ + { + kind: 'InputObjectDefinition', + name: nameNode('Hello', loc(7, 12)), + fields: [ + inputFieldNode( + nameNode('world', loc(17, 22)), + typeNode('String', loc(24, 30)), + loc(17, 30) + ) + ], + loc: loc(1, 32), + } + ], + loc: loc(1, 32), + }; + expect(printJson(expected)).to.equal(printJson(doc)); + }); + + it('Simple input object with args should fail', () => { + var body = ` +input Hello { + world(foo: Int): String +}`; + expect(() => parseSchema(body)).to.throw('Error'); + }); + + it('Reject query keywords', () => { + var body = `query Foo { field }`; + expect(() => parseSchema(body)).to.throw('Error'); + }); + + it('Reject query shorthand', () => { + var body = `{ field }`; + expect(() => parseSchema(body)).to.throw('Error'); + }); +}); diff --git a/src/language/schema/ast.js b/src/language/schema/ast.js new file mode 100644 index 0000000000..61f4a881f7 --- /dev/null +++ b/src/language/schema/ast.js @@ -0,0 +1,92 @@ +/*@flow*/ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import type { + Location, + Name, + Type, + NamedType +} from '../ast'; + +export type SchemaDocument = { + kind: 'Schema Document'; + loc?: ?Location; + definitions: Array; +} + +export type SchemaDefinition = + TypeDefinition | + InterfaceDefinition | + EnumDefinition | + ScalarDefinition | + InputObjectDefinition + +export type TypeDefinition = { + kind: 'TypeDefinition'; + loc?: ?Location; + name: Name; + interfaces?: ?Array; + fields: Array; +} + +export type InterfaceDefinition = { + kind: 'InterfaceDefinition'; + loc?: ?Location; + name: Name; + fields: Array; +} + +export type FieldDefinition = { + kind: 'FieldDefinition'; + loc?: ?Location; + name: Name; + type: Type; + arguments: Array; +} + +export type InputFieldDefinition = { + kind: 'InputFieldDefinition'; + loc?: ?Location; + name: Name; + type: Type; +} + +export type ArgumentDefinition = { + kind: 'ArgumentDefinition'; + loc?: ?Location; + name: Name; + type: Type; +} + +export type EnumDefinition = { + kind: 'EnumDefinition'; + loc?: ?Location; + name: Name; + values: Array; +} + +export type EnumValueDefinition = { + kind: 'EnumValueDefinition'; + loc?: ?Location; + name: Name; +} + +export type ScalarDefinition = { + kind: 'ScalarDefinition'; + loc?: ?Location; + name: Name; +} + +export type InputObjectDefinition = { + kind: 'InputObjectDefinition'; + loc?: ?Location; + name: Name; + fields: Array; +} diff --git a/src/language/schema/kinds.js b/src/language/schema/kinds.js new file mode 100644 index 0000000000..dfbf62bc95 --- /dev/null +++ b/src/language/schema/kinds.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// Schema + +export const SCHEMA_DOCUMENT = 'SchemaDocument'; +export const FIELD_DEFINITION = 'FieldDefinition'; +export const TYPE_DEFINITION = 'TypeDefinition'; +export const INTERFACE_DEFINITION = 'InterfaceDefinition'; +export const ENUM_DEFINITION = 'EnumDefinition'; +export const ENUM_VALUE_DEFINITION = 'EnumValueDefinition'; +export const ARGUMENT_DEFINITION = 'ArgumentDefinition'; +export const UNION_DEFINITION = 'UnionDefinition'; +export const SCALAR_DEFINITION = 'ScalarDefinition'; +export const INPUT_OBJECT_DEFINITION = 'InputObjectDefinition'; +export const INPUT_FIELD_DEFINITION = 'InputFieldDefinition'; diff --git a/src/language/schema/parser.js b/src/language/schema/parser.js new file mode 100644 index 0000000000..c8fcfd2795 --- /dev/null +++ b/src/language/schema/parser.js @@ -0,0 +1,294 @@ +/* @flow */ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import { Source } from './../source'; + +import { TokenKind } from './../lexer'; + +import { + ParseOptions, + makeParser, + peek, + skip, + loc, + any, + many, + expect, + unexpected, + expectKeyword, + advance, +} from './../parserCore'; + +import { + parseName, + parseNamedType, + parseType, +} from './../parser'; + +import { NamedType } from '../ast'; + +import type { + SchemaDocument, + TypeDefinition, + FieldDefinition, + EnumDefinition, + EnumValueDefinition, + InterfaceDefinition, + ArgumentDefinition, + ScalarDefinition, + InputObjectDefinition, + InputFieldDefinition, +} from './ast'; + +import { + SCHEMA_DOCUMENT, + ENUM_DEFINITION, + ENUM_VALUE_DEFINITION, + TYPE_DEFINITION, + INTERFACE_DEFINITION, + FIELD_DEFINITION, + ARGUMENT_DEFINITION, + UNION_DEFINITION, + SCALAR_DEFINITION, + INPUT_OBJECT_DEFINITION, + INPUT_FIELD_DEFINITION, +} from './kinds'; + +export function parseSchema( + source: Source | string, + options?: ParseOptions +): SchemaDocument { + var sourceObj = source instanceof Source ? source : new Source(source); + var parser = makeParser(sourceObj, options || {}); + return parseSchemaDocument(parser); +} + +function parseSchemaDocument(parser): SchemaDocument { + var start = parser.token.start; + var definitions = []; + do { + definitions.push(parseSchemaDefinition(parser)); + } while (!skip(parser, TokenKind.EOF)); + + return { + kind: SCHEMA_DOCUMENT, + definitions, + loc: loc(parser, start) + }; +} + + +function parseSchemaDefinition(parser): any { + if (!peek(parser, TokenKind.NAME)) { + throw unexpected(parser); + } + switch (parser.token.value) { + case 'type': + return parseTypeDefinition(parser); + case 'enum': + return parseEnumDefinition(parser); + case 'interface': + return parseInterfaceDefinition(parser); + case 'union': + return parseUnionDefinition(parser); + case 'input': + return parseInputObjectDefinition(parser); + case 'scalar': + return parseScalarDefinition(parser); + default: + throw unexpected(parser); + } +} + +function parseTypeDefinition(parser): TypeDefinition { + var start = parser.token.start; + expectKeyword(parser, 'type'); + var name = parseName(parser); + var interfaces = parseInterfaces(parser); + var fields = any( + parser, + TokenKind.BRACE_L, + parseFieldDefinition, + TokenKind.BRACE_R); + return { + kind: TYPE_DEFINITION, + name, + interfaces: interfaces, + fields: fields, + loc: loc(parser, start), + }; +} + +function parseFieldDefinition(parser): FieldDefinition { + var start = parser.token.start; + var name = parseName(parser); + var args = parseArgumentDefs(parser); + expect(parser, TokenKind.COLON); + var type = parseType(parser); + var location = loc(parser, start); + return { + kind: FIELD_DEFINITION, + name: name, + type: type, + arguments: args, + loc: location, + }; +} + +function parseArgumentDefs(parser): Array { + if (!peek(parser, TokenKind.PAREN_L)) { + return []; + } + return many( + parser, + TokenKind.PAREN_L, + parseArgumentDef, + TokenKind.PAREN_R + ); +} + +function parseArgumentDef(parser): ArgumentDefinition { + var start = parser.token.start; + var name = parseName(parser); + expect(parser, TokenKind.COLON); + var type = parseType(parser, false); + var location = loc(parser, start); + return { + kind: ARGUMENT_DEFINITION, + name: name, + type: type, + loc: location, + }; +} + +function parseInterfaces(parser): Array { + var types = []; + if (parser.token.value === 'implements') { + advance(parser); + do { + types.push(parseType(parser)); + } while (!peek(parser, TokenKind.BRACE_L)); + } + return types; +} + +function parseEnumDefinition(parser): EnumDefinition { + var start = parser.token.start; + expectKeyword(parser, 'enum'); + var name = parseName(parser); + var values = many( + parser, + TokenKind.BRACE_L, + parseEnumValueDefinition, + TokenKind.BRACE_R); + var location = loc(parser, start); + return { + kind: ENUM_DEFINITION, + name: name, + values: values, + loc: location, + }; +} + +function parseEnumValueDefinition(parser) : EnumValueDefinition { + var start = parser.token.start; + var name = parseName(parser); + var location = loc(parser, start); + return { + kind: ENUM_VALUE_DEFINITION, + name: name, + loc: location, + }; +} + +function parseInterfaceDefinition(parser): InterfaceDefinition { + var start = parser.token.start; + expectKeyword(parser, 'interface'); + var name = parseName(parser); + var fields = any( + parser, + TokenKind.BRACE_L, + parseFieldDefinition, + TokenKind.BRACE_R); + return { + kind: INTERFACE_DEFINITION, + name, + fields: fields, + loc: loc(parser, start), + }; +} + +function parseUnionDefinition(parser) { + var start = parser.token.start; + expectKeyword(parser, 'union'); + var name = parseName(parser); + var types = parseUnionMembers(parser); + var location = loc(parser, start); + return { + kind: UNION_DEFINITION, + name: name, + types: types, + loc: location, + }; +} + +function parseUnionMembers(parser) { + expect(parser, TokenKind.BRACE_L); + var members = [(parseNamedType(parser))]; + while (!skip(parser, TokenKind.BRACE_R)) { + expect(parser, TokenKind.PIPE); + members.push((parseNamedType(parser))); + } + return members; +} + + +function parseInputObjectDefinition(parser): InputObjectDefinition { + var start = parser.token.start; + expectKeyword(parser, 'input'); + var name = parseName(parser); + var fields = any( + parser, + TokenKind.BRACE_L, + parseInputFieldDefinition, + TokenKind.BRACE_R); + return { + kind: INPUT_OBJECT_DEFINITION, + name, + fields: fields, + loc: loc(parser, start), + }; +} + +function parseInputFieldDefinition(parser): InputFieldDefinition { + var start = parser.token.start; + var name = parseName(parser); + expect(parser, TokenKind.COLON); + var type = parseType(parser); + var location = loc(parser, start); + return { + kind: INPUT_FIELD_DEFINITION, + name: name, + type: type, + loc: location, + }; +} + +function parseScalarDefinition(parser): ScalarDefinition { + var start = parser.token.start; + expectKeyword(parser, 'scalar'); + var name = parseName(parser); + var location = loc(parser, start); + return { + kind: SCALAR_DEFINITION, + name: name, + loc: location, + }; +}