Skip to content

Commit a930eec

Browse files
committed
RFC: SDL - Separate multiple inherited interfaces with &
This replaces: ```graphql type Foo implements Bar, Baz { field: Type } ``` With: ```graphql type Foo implements Bar & Baz { field: Type } ``` With no changes to the common case of implementing a single interface. This is more consistent with other trailing lists of values which either have an explicit separator (union members) or are prefixed with a sigil (directives). This avoids parse ambiguity in the case of an omitted field set, illustrated by #1166 This is a breaking change for existing uses of multiple inheritence. To allow for an adaptive migration, this adds a parse option to continue to support the existing experimental SDL: `parse(source, {legacySDL: true})`
1 parent 8173c24 commit a930eec

File tree

8 files changed

+119
-26
lines changed

8 files changed

+119
-26
lines changed

src/language/__tests__/schema-kitchen-sink.graphql

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ schema {
1212
This is a description
1313
of the `Foo` type.
1414
"""
15-
type Foo implements Bar {
15+
type Foo implements Bar & Baz {
1616
one: Type
1717
two(argument: InputType!): Type
1818
three(argument: InputType, other: String): Int

src/language/__tests__/schema-parser-test.js

+69-7
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ type Hello {
291291
});
292292

293293
it('Simple type inheriting multiple interfaces', () => {
294-
const body = 'type Hello implements Wo, rld { field: String }';
294+
const body = 'type Hello implements Wo & rld { field: String }';
295295
const doc = parse(body);
296296
const expected = {
297297
kind: 'Document',
@@ -301,20 +301,49 @@ type Hello {
301301
name: nameNode('Hello', { start: 5, end: 10 }),
302302
interfaces: [
303303
typeNode('Wo', { start: 22, end: 24 }),
304-
typeNode('rld', { start: 26, end: 29 }),
304+
typeNode('rld', { start: 27, end: 30 }),
305305
],
306306
directives: [],
307307
fields: [
308308
fieldNode(
309-
nameNode('field', { start: 32, end: 37 }),
310-
typeNode('String', { start: 39, end: 45 }),
311-
{ start: 32, end: 45 },
309+
nameNode('field', { start: 33, end: 38 }),
310+
typeNode('String', { start: 40, end: 46 }),
311+
{ start: 33, end: 46 },
312312
),
313313
],
314-
loc: { start: 0, end: 47 },
314+
loc: { start: 0, end: 48 },
315315
},
316316
],
317-
loc: { start: 0, end: 47 },
317+
loc: { start: 0, end: 48 },
318+
};
319+
expect(printJson(doc)).to.equal(printJson(expected));
320+
});
321+
322+
it('Simple type inheriting multiple interfaces with leading ampersand', () => {
323+
const body = 'type Hello implements & Wo & rld { field: String }';
324+
const doc = parse(body);
325+
const expected = {
326+
kind: 'Document',
327+
definitions: [
328+
{
329+
kind: 'ObjectTypeDefinition',
330+
name: nameNode('Hello', { start: 5, end: 10 }),
331+
interfaces: [
332+
typeNode('Wo', { start: 24, end: 26 }),
333+
typeNode('rld', { start: 29, end: 32 }),
334+
],
335+
directives: [],
336+
fields: [
337+
fieldNode(
338+
nameNode('field', { start: 35, end: 40 }),
339+
typeNode('String', { start: 42, end: 48 }),
340+
{ start: 35, end: 48 },
341+
),
342+
],
343+
loc: { start: 0, end: 50 },
344+
},
345+
],
346+
loc: { start: 0, end: 50 },
318347
};
319348
expect(printJson(doc)).to.equal(printJson(expected));
320349
});
@@ -708,4 +737,37 @@ input Hello {
708737
{ line: 2, column: 33 },
709738
);
710739
});
740+
741+
describe('Option: legacySDL', () => {
742+
it('Supports type inheriting multiple interfaces with no ampersand', () => {
743+
const body = 'type Hello implements Wo rld { field: String }';
744+
expect(() => parse(body)).to.throw('Syntax Error: Unexpected Name "rld"');
745+
const doc = parse(body, { legacySDL: true });
746+
expect(doc).to.containSubset({
747+
definitions: [
748+
{
749+
interfaces: [
750+
typeNode('Wo', { start: 22, end: 24 }),
751+
typeNode('rld', { start: 25, end: 28 }),
752+
],
753+
},
754+
],
755+
});
756+
});
757+
758+
it('Supports type with empty fields', () => {
759+
const body = 'type Hello { }';
760+
expect(() => parse(body)).to.throw(
761+
'Syntax Error: Expected Name, found }',
762+
);
763+
const doc = parse(body, { legacySDL: true });
764+
expect(doc).to.containSubset({
765+
definitions: [
766+
{
767+
fields: [],
768+
},
769+
],
770+
});
771+
});
772+
});
711773
});

src/language/__tests__/schema-printer-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ describe('Printer', () => {
5656
This is a description
5757
of the \`Foo\` type.
5858
"""
59-
type Foo implements Bar {
59+
type Foo implements Bar & Baz {
6060
one: Type
6161
two(argument: InputType!): Type
6262
three(argument: InputType, other: String): Int

src/language/ast.js

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type TokenKind =
5050
| '<EOF>'
5151
| '!'
5252
| '$'
53+
| '&'
5354
| '('
5455
| ')'
5556
| '...'

src/language/lexer.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ const SOF = '<SOF>';
9999
const EOF = '<EOF>';
100100
const BANG = '!';
101101
const DOLLAR = '$';
102+
const AMP = '&';
102103
const PAREN_L = '(';
103104
const PAREN_R = ')';
104105
const SPREAD = '...';
@@ -126,6 +127,7 @@ export const TokenKind = {
126127
EOF,
127128
BANG,
128129
DOLLAR,
130+
AMP,
129131
PAREN_L,
130132
PAREN_R,
131133
SPREAD,
@@ -160,7 +162,7 @@ const slice = String.prototype.slice;
160162
* Helper function for constructing the Token object.
161163
*/
162164
function Tok(
163-
kind,
165+
kind: $Values<typeof TokenKind>,
164166
start: number,
165167
end: number,
166168
line: number,
@@ -242,6 +244,9 @@ function readToken(lexer: Lexer<*>, prev: Token): Token {
242244
// $
243245
case 36:
244246
return new Tok(DOLLAR, position, position + 1, line, col, prev);
247+
// &
248+
case 38:
249+
return new Tok(AMP, position, position + 1, line, col, prev);
245250
// (
246251
case 40:
247252
return new Tok(PAREN_L, position, position + 1, line, col, prev);

src/language/parser.js

+37-12
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ export type ParseOptions = {
119119
*/
120120
noLocation?: boolean,
121121

122+
/**
123+
* If enabled, the parser will parse legacy Schema Definition Language.
124+
* Otherwise, the parser will follow the current specification.
125+
*
126+
* Note: this option is provided to ease adoption and may be removed in a
127+
* future release.
128+
*/
129+
legacySDL?: boolean,
130+
122131
/**
123132
* EXPERIMENTAL:
124133
*
@@ -912,15 +921,23 @@ function parseObjectTypeDefinition(lexer: Lexer<*>): ObjectTypeDefinitionNode {
912921
}
913922

914923
/**
915-
* ImplementsInterfaces : implements NamedType+
924+
* ImplementsInterfaces :
925+
* - implements `&`? NamedType
926+
* - ImplementsInterfaces & NamedType
916927
*/
917928
function parseImplementsInterfaces(lexer: Lexer<*>): Array<NamedTypeNode> {
918929
const types = [];
919930
if (lexer.token.value === 'implements') {
920931
lexer.advance();
932+
// Optional leading ampersand
933+
skip(lexer, TokenKind.AMP);
921934
do {
922935
types.push(parseNamedType(lexer));
923-
} while (peek(lexer, TokenKind.NAME));
936+
} while (
937+
skip(lexer, TokenKind.AMP) ||
938+
// Legacy support for the SDL?
939+
(lexer.options.legacySDL && peek(lexer, TokenKind.NAME))
940+
);
924941
}
925942
return types;
926943
}
@@ -929,6 +946,16 @@ function parseImplementsInterfaces(lexer: Lexer<*>): Array<NamedTypeNode> {
929946
* FieldsDefinition : { FieldDefinition+ }
930947
*/
931948
function parseFieldsDefinition(lexer: Lexer<*>): Array<FieldDefinitionNode> {
949+
// Legacy support for the SDL?
950+
if (
951+
lexer.options.legacySDL &&
952+
peek(lexer, TokenKind.BRACE_L) &&
953+
lexer.lookahead().kind === TokenKind.BRACE_R
954+
) {
955+
lexer.advance();
956+
lexer.advance();
957+
return [];
958+
}
932959
return peek(lexer, TokenKind.BRACE_L)
933960
? many(lexer, TokenKind.BRACE_L, parseFieldDefinition, TokenKind.BRACE_R)
934961
: [];
@@ -1018,15 +1045,15 @@ function parseInterfaceTypeDefinition(
10181045

10191046
/**
10201047
* UnionTypeDefinition :
1021-
* - Description? union Name Directives[Const]? MemberTypesDefinition?
1048+
* - Description? union Name Directives[Const]? UnionMemberTypes?
10221049
*/
10231050
function parseUnionTypeDefinition(lexer: Lexer<*>): UnionTypeDefinitionNode {
10241051
const start = lexer.token;
10251052
const description = parseDescription(lexer);
10261053
expectKeyword(lexer, 'union');
10271054
const name = parseName(lexer);
10281055
const directives = parseDirectives(lexer, true);
1029-
const types = parseMemberTypesDefinition(lexer);
1056+
const types = parseUnionMemberTypes(lexer);
10301057
return {
10311058
kind: UNION_TYPE_DEFINITION,
10321059
description,
@@ -1038,13 +1065,11 @@ function parseUnionTypeDefinition(lexer: Lexer<*>): UnionTypeDefinitionNode {
10381065
}
10391066

10401067
/**
1041-
* MemberTypesDefinition : = MemberTypes
1042-
*
1043-
* MemberTypes :
1044-
* - `|`? NamedType
1045-
* - MemberTypes | NamedType
1068+
* UnionMemberTypes :
1069+
* - = `|`? NamedType
1070+
* - UnionMemberTypes | NamedType
10461071
*/
1047-
function parseMemberTypesDefinition(lexer: Lexer<*>): Array<NamedTypeNode> {
1072+
function parseUnionMemberTypes(lexer: Lexer<*>): Array<NamedTypeNode> {
10481073
const types = [];
10491074
if (skip(lexer, TokenKind.EQUALS)) {
10501075
// Optional leading pipe
@@ -1258,7 +1283,7 @@ function parseInterfaceTypeExtension(
12581283

12591284
/**
12601285
* UnionTypeExtension :
1261-
* - extend union Name Directives[Const]? MemberTypesDefinition
1286+
* - extend union Name Directives[Const]? UnionMemberTypes
12621287
* - extend union Name Directives[Const]
12631288
*/
12641289
function parseUnionTypeExtension(lexer: Lexer<*>): UnionTypeExtensionNode {
@@ -1267,7 +1292,7 @@ function parseUnionTypeExtension(lexer: Lexer<*>): UnionTypeExtensionNode {
12671292
expectKeyword(lexer, 'union');
12681293
const name = parseName(lexer);
12691294
const directives = parseDirectives(lexer, true);
1270-
const types = parseMemberTypesDefinition(lexer);
1295+
const types = parseUnionMemberTypes(lexer);
12711296
if (directives.length === 0 && types.length === 0) {
12721297
throw unexpected(lexer);
12731298
}

src/language/printer.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const printDocASTReducer = {
130130
[
131131
'type',
132132
name,
133-
wrap('implements ', join(interfaces, ', ')),
133+
wrap('implements ', join(interfaces, ' & ')),
134134
join(directives, ' '),
135135
block(fields),
136136
],
@@ -226,7 +226,7 @@ const printDocASTReducer = {
226226
[
227227
'extend type',
228228
name,
229-
wrap('implements ', join(interfaces, ', ')),
229+
wrap('implements ', join(interfaces, ' & ')),
230230
join(directives, ' '),
231231
block(fields),
232232
],

src/type/__tests__/validation-test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -822,14 +822,14 @@ describe('Type System: Objects can only implement unique interfaces', () => {
822822
field: String
823823
}
824824
825-
type AnotherObject implements AnotherInterface, AnotherInterface {
825+
type AnotherObject implements AnotherInterface & AnotherInterface {
826826
field: String
827827
}
828828
`);
829829
expect(validateSchema(schema)).to.containSubset([
830830
{
831831
message: 'Type AnotherObject can only implement AnotherInterface once.',
832-
locations: [{ line: 10, column: 37 }, { line: 10, column: 55 }],
832+
locations: [{ line: 10, column: 37 }, { line: 10, column: 56 }],
833833
},
834834
]);
835835
});

0 commit comments

Comments
 (0)