diff --git a/src/language/__tests__/schema-kitchen-sink.graphql b/src/language/__tests__/schema-kitchen-sink.graphql index f18b09968b..f94f47c8e5 100644 --- a/src/language/__tests__/schema-kitchen-sink.graphql +++ b/src/language/__tests__/schema-kitchen-sink.graphql @@ -12,7 +12,7 @@ schema { This is a description of the `Foo` type. """ -type Foo implements Bar { +type Foo implements Bar & Baz { one: Type two(argument: InputType!): Type three(argument: InputType, other: String): Int diff --git a/src/language/__tests__/schema-parser-test.js b/src/language/__tests__/schema-parser-test.js index b94286ed16..b6bec6a39e 100644 --- a/src/language/__tests__/schema-parser-test.js +++ b/src/language/__tests__/schema-parser-test.js @@ -291,7 +291,7 @@ type Hello { }); it('Simple type inheriting multiple interfaces', () => { - const body = 'type Hello implements Wo, rld { field: String }'; + const body = 'type Hello implements Wo & rld { field: String }'; const doc = parse(body); const expected = { kind: 'Document', @@ -301,20 +301,49 @@ type Hello { name: nameNode('Hello', { start: 5, end: 10 }), interfaces: [ typeNode('Wo', { start: 22, end: 24 }), - typeNode('rld', { start: 26, end: 29 }), + typeNode('rld', { start: 27, end: 30 }), ], directives: [], fields: [ fieldNode( - nameNode('field', { start: 32, end: 37 }), - typeNode('String', { start: 39, end: 45 }), - { start: 32, end: 45 }, + nameNode('field', { start: 33, end: 38 }), + typeNode('String', { start: 40, end: 46 }), + { start: 33, end: 46 }, ), ], - loc: { start: 0, end: 47 }, + loc: { start: 0, end: 48 }, }, ], - loc: { start: 0, end: 47 }, + loc: { start: 0, end: 48 }, + }; + expect(printJson(doc)).to.equal(printJson(expected)); + }); + + it('Simple type inheriting multiple interfaces with leading ampersand', () => { + const body = 'type Hello implements & Wo & rld { field: String }'; + const doc = parse(body); + const expected = { + kind: 'Document', + definitions: [ + { + kind: 'ObjectTypeDefinition', + name: nameNode('Hello', { start: 5, end: 10 }), + interfaces: [ + typeNode('Wo', { start: 24, end: 26 }), + typeNode('rld', { start: 29, end: 32 }), + ], + directives: [], + fields: [ + fieldNode( + nameNode('field', { start: 35, end: 40 }), + typeNode('String', { start: 42, end: 48 }), + { start: 35, end: 48 }, + ), + ], + loc: { start: 0, end: 50 }, + }, + ], + loc: { start: 0, end: 50 }, }; expect(printJson(doc)).to.equal(printJson(expected)); }); @@ -708,4 +737,37 @@ input Hello { { line: 2, column: 33 }, ); }); + + describe('Option: legacySDL', () => { + it('Supports type inheriting multiple interfaces with no ampersand', () => { + const body = 'type Hello implements Wo rld { field: String }'; + expect(() => parse(body)).to.throw('Syntax Error: Unexpected Name "rld"'); + const doc = parse(body, { legacySDL: true }); + expect(doc).to.containSubset({ + definitions: [ + { + interfaces: [ + typeNode('Wo', { start: 22, end: 24 }), + typeNode('rld', { start: 25, end: 28 }), + ], + }, + ], + }); + }); + + it('Supports type with empty fields', () => { + const body = 'type Hello { }'; + expect(() => parse(body)).to.throw( + 'Syntax Error: Expected Name, found }', + ); + const doc = parse(body, { legacySDL: true }); + expect(doc).to.containSubset({ + definitions: [ + { + fields: [], + }, + ], + }); + }); + }); }); diff --git a/src/language/__tests__/schema-printer-test.js b/src/language/__tests__/schema-printer-test.js index f57c824fb7..1bacb8e586 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -56,7 +56,7 @@ describe('Printer', () => { This is a description of the \`Foo\` type. """ - type Foo implements Bar { + type Foo implements Bar & Baz { one: Type two(argument: InputType!): Type three(argument: InputType, other: String): Int diff --git a/src/language/ast.js b/src/language/ast.js index 01d17cdbe8..bec5ae3d57 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -50,6 +50,7 @@ type TokenKind = | '' | '!' | '$' + | '&' | '(' | ')' | '...' diff --git a/src/language/lexer.js b/src/language/lexer.js index 8b2bd99f44..45e69ac59b 100644 --- a/src/language/lexer.js +++ b/src/language/lexer.js @@ -99,6 +99,7 @@ const SOF = ''; const EOF = ''; const BANG = '!'; const DOLLAR = '$'; +const AMP = '&'; const PAREN_L = '('; const PAREN_R = ')'; const SPREAD = '...'; @@ -126,6 +127,7 @@ export const TokenKind = { EOF, BANG, DOLLAR, + AMP, PAREN_L, PAREN_R, SPREAD, @@ -160,7 +162,7 @@ const slice = String.prototype.slice; * Helper function for constructing the Token object. */ function Tok( - kind, + kind: $Values, start: number, end: number, line: number, @@ -242,6 +244,9 @@ function readToken(lexer: Lexer<*>, prev: Token): Token { // $ case 36: return new Tok(DOLLAR, position, position + 1, line, col, prev); + // & + case 38: + return new Tok(AMP, position, position + 1, line, col, prev); // ( case 40: return new Tok(PAREN_L, position, position + 1, line, col, prev); diff --git a/src/language/parser.js b/src/language/parser.js index 0cf6225a64..89c1490e00 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -119,6 +119,15 @@ export type ParseOptions = { */ noLocation?: boolean, + /** + * If enabled, the parser will parse legacy Schema Definition Language. + * Otherwise, the parser will follow the current specification. + * + * Note: this option is provided to ease adoption and may be removed in a + * future release. + */ + legacySDL?: boolean, + /** * EXPERIMENTAL: * @@ -912,15 +921,23 @@ function parseObjectTypeDefinition(lexer: Lexer<*>): ObjectTypeDefinitionNode { } /** - * ImplementsInterfaces : implements NamedType+ + * ImplementsInterfaces : + * - implements `&`? NamedType + * - InterfaceTypes & NamedType */ function parseImplementsInterfaces(lexer: Lexer<*>): Array { const types = []; if (lexer.token.value === 'implements') { lexer.advance(); + // Optional leading ampersand + skip(lexer, TokenKind.AMP); do { types.push(parseNamedType(lexer)); - } while (peek(lexer, TokenKind.NAME)); + } while ( + skip(lexer, TokenKind.AMP) || + // Legacy support for the SDL? + (lexer.options.legacySDL && peek(lexer, TokenKind.NAME)) + ); } return types; } @@ -929,6 +946,16 @@ function parseImplementsInterfaces(lexer: Lexer<*>): Array { * FieldsDefinition : { FieldDefinition+ } */ function parseFieldsDefinition(lexer: Lexer<*>): Array { + // Legacy support for the SDL? + if ( + lexer.options.legacySDL && + peek(lexer, TokenKind.BRACE_L) && + lexer.lookahead().kind === TokenKind.BRACE_R + ) { + lexer.advance(); + lexer.advance(); + return []; + } return peek(lexer, TokenKind.BRACE_L) ? many(lexer, TokenKind.BRACE_L, parseFieldDefinition, TokenKind.BRACE_R) : []; @@ -1018,7 +1045,7 @@ function parseInterfaceTypeDefinition( /** * UnionTypeDefinition : - * - Description? union Name Directives[Const]? MemberTypesDefinition? + * - Description? union Name Directives[Const]? UnionMemberTypes? */ function parseUnionTypeDefinition(lexer: Lexer<*>): UnionTypeDefinitionNode { const start = lexer.token; @@ -1026,7 +1053,7 @@ function parseUnionTypeDefinition(lexer: Lexer<*>): UnionTypeDefinitionNode { expectKeyword(lexer, 'union'); const name = parseName(lexer); const directives = parseDirectives(lexer, true); - const types = parseMemberTypesDefinition(lexer); + const types = parseUnionMemberTypes(lexer); return { kind: UNION_TYPE_DEFINITION, description, @@ -1038,13 +1065,11 @@ function parseUnionTypeDefinition(lexer: Lexer<*>): UnionTypeDefinitionNode { } /** - * MemberTypesDefinition : = MemberTypes - * - * MemberTypes : - * - `|`? NamedType - * - MemberTypes | NamedType + * UnionMemberTypes : + * - = `|`? NamedType + * - UnionMemberTypes | NamedType */ -function parseMemberTypesDefinition(lexer: Lexer<*>): Array { +function parseUnionMemberTypes(lexer: Lexer<*>): Array { const types = []; if (skip(lexer, TokenKind.EQUALS)) { // Optional leading pipe @@ -1258,7 +1283,7 @@ function parseInterfaceTypeExtension( /** * UnionTypeExtension : - * - extend union Name Directives[Const]? MemberTypesDefinition + * - extend union Name Directives[Const]? UnionMemberTypes * - extend union Name Directives[Const] */ function parseUnionTypeExtension(lexer: Lexer<*>): UnionTypeExtensionNode { @@ -1267,7 +1292,7 @@ function parseUnionTypeExtension(lexer: Lexer<*>): UnionTypeExtensionNode { expectKeyword(lexer, 'union'); const name = parseName(lexer); const directives = parseDirectives(lexer, true); - const types = parseMemberTypesDefinition(lexer); + const types = parseUnionMemberTypes(lexer); if (directives.length === 0 && types.length === 0) { throw unexpected(lexer); } diff --git a/src/language/printer.js b/src/language/printer.js index 8dcf1bba67..10bb63595a 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -130,7 +130,7 @@ const printDocASTReducer = { [ 'type', name, - wrap('implements ', join(interfaces, ', ')), + wrap('implements ', join(interfaces, ' & ')), join(directives, ' '), block(fields), ], @@ -226,7 +226,7 @@ const printDocASTReducer = { [ 'extend type', name, - wrap('implements ', join(interfaces, ', ')), + wrap('implements ', join(interfaces, ' & ')), join(directives, ' '), block(fields), ], diff --git a/src/type/__tests__/validation-test.js b/src/type/__tests__/validation-test.js index 107e94e331..5e0572d4e0 100644 --- a/src/type/__tests__/validation-test.js +++ b/src/type/__tests__/validation-test.js @@ -822,14 +822,14 @@ describe('Type System: Objects can only implement unique interfaces', () => { field: String } - type AnotherObject implements AnotherInterface, AnotherInterface { + type AnotherObject implements AnotherInterface & AnotherInterface { field: String } `); expect(validateSchema(schema)).to.containSubset([ { message: 'Type AnotherObject can only implement AnotherInterface once.', - locations: [{ line: 10, column: 37 }, { line: 10, column: 55 }], + locations: [{ line: 10, column: 37 }, { line: 10, column: 56 }], }, ]); });