RFC: SDL - Separate multiple inherited interfaces with &

This commit is contained in:
Vladimir Razuvaev 2018-08-08 01:11:47 +07:00
parent 8e02fdc537
commit a19fc3d208
9 changed files with 113 additions and 16 deletions

View File

@ -3,6 +3,22 @@
### Breaking: minimum supported version of PHP ### Breaking: minimum supported version of PHP
New minimum required version of PHP is **7.1+** New minimum required version of PHP is **7.1+**
### Breaking: multiple interfaces separated with & in SDL
Before the change:
```graphql
type Foo implements Bar, Baz { field: Type }
```
After the change:
```graphql
type Foo implements Bar & Baz { field: Type }
```
To allow for an adaptive migration, use `allowLegacySDLImplementsInterfaces` option of parser:
```php
Parser::parse($source, [ 'allowLegacySDLImplementsInterfaces' => true])
```
### Breaking: errors formatting changed according to spec ### Breaking: errors formatting changed according to spec
Extensions assigned to errors are shown under `extensions` key Extensions assigned to errors are shown under `extensions` key
```php ```php

View File

@ -147,6 +147,8 @@ class Lexer
return $this->readComment($line, $col, $prev); return $this->readComment($line, $col, $prev);
case 36: // $ case 36: // $
return new Token(Token::DOLLAR, $position, $position + 1, $line, $col, $prev); return new Token(Token::DOLLAR, $position, $position + 1, $line, $col, $prev);
case 38: // &
return new Token(Token::AMP, $position, $position + 1, $line, $col, $prev);
case 40: // ( case 40: // (
return new Token(Token::PAREN_L, $position, $position + 1, $line, $col, $prev); return new Token(Token::PAREN_L, $position, $position + 1, $line, $col, $prev);
case 41: // ) case 41: // )

View File

@ -66,7 +66,6 @@ class Parser
* in the source that they correspond to. This configuration flag * in the source that they correspond to. This configuration flag
* disables that behavior for performance or testing.) * disables that behavior for performance or testing.)
* *
*
* allowLegacySDLEmptyFields: boolean * allowLegacySDLEmptyFields: boolean
* If enabled, the parser will parse empty fields sets in the Schema * If enabled, the parser will parse empty fields sets in the Schema
* Definition Language. Otherwise, the parser will follow the current * Definition Language. Otherwise, the parser will follow the current
@ -75,6 +74,14 @@ class Parser
* This option is provided to ease adoption of the final SDL specification * This option is provided to ease adoption of the final SDL specification
* and will be removed in a future major release. * and will be removed in a future major release.
* *
* allowLegacySDLImplementsInterfaces: boolean
* If enabled, the parser will parse implemented interfaces with no `&`
* character between each interface. Otherwise, the parser will follow the
* current specification.
*
* This option is provided to ease adoption of the final SDL specification
* and will be removed in a future major release.
*
* experimentalFragmentVariables: boolean, * experimentalFragmentVariables: boolean,
* (If enabled, the parser will understand and parse variable definitions * (If enabled, the parser will understand and parse variable definitions
* contained in a fragment definition. They'll be represented in the * contained in a fragment definition. They'll be represented in the
@ -1072,6 +1079,10 @@ class Parser
} }
/** /**
* ImplementsInterfaces :
* - implements `&`? NamedType
* - ImplementsInterfaces & NamedType
*
* @return NamedTypeNode[] * @return NamedTypeNode[]
*/ */
function parseImplementsInterfaces() function parseImplementsInterfaces()
@ -1079,9 +1090,15 @@ class Parser
$types = []; $types = [];
if ($this->lexer->token->value === 'implements') { if ($this->lexer->token->value === 'implements') {
$this->lexer->advance(); $this->lexer->advance();
// Optional leading ampersand
$this->skip(Token::AMP);
do { do {
$types[] = $this->parseNamedType(); $types[] = $this->parseNamedType();
} while ($this->peek(Token::NAME)); } while (
$this->skip(Token::AMP) ||
// Legacy support for the SDL?
(!empty($this->lexer->options['allowLegacySDLImplementsInterfaces']) && $this->peek(Token::NAME))
);
} }
return $types; return $types;
} }

View File

@ -212,7 +212,7 @@ class Printer
$this->join([ $this->join([
'type', 'type',
$def->name, $def->name,
$this->wrap('implements ', $this->join($def->interfaces, ', ')), $this->wrap('implements ', $this->join($def->interfaces, ' & ')),
$this->join($def->directives, ' '), $this->join($def->directives, ' '),
$this->block($def->fields) $this->block($def->fields)
], ' ') ], ' ')
@ -300,7 +300,7 @@ class Printer
return $this->join([ return $this->join([
'extend type', 'extend type',
$def->name, $def->name,
$this->wrap('implements ', $this->join($def->interfaces, ', ')), $this->wrap('implements ', $this->join($def->interfaces, ' & ')),
$this->join($def->directives, ' '), $this->join($def->directives, ' '),
$this->block($def->fields), $this->block($def->fields),
], ' '); ], ' ');

View File

@ -12,6 +12,7 @@ class Token
const EOF = '<EOF>'; const EOF = '<EOF>';
const BANG = '!'; const BANG = '!';
const DOLLAR = '$'; const DOLLAR = '$';
const AMP = '&';
const PAREN_L = '('; const PAREN_L = '(';
const PAREN_R = ')'; const PAREN_R = ')';
const SPREAD = '...'; const SPREAD = '...';

View File

@ -330,7 +330,7 @@ type Hello {
*/ */
public function testSimpleTypeInheritingMultipleInterfaces() public function testSimpleTypeInheritingMultipleInterfaces()
{ {
$body = 'type Hello implements Wo, rld { field: String }'; $body = 'type Hello implements Wo & rld { field: String }';
$loc = function($start, $end) {return TestUtils::locArray($start, $end);}; $loc = function($start, $end) {return TestUtils::locArray($start, $end);};
$doc = Parser::parse($body); $doc = Parser::parse($body);
@ -341,27 +341,62 @@ type Hello {
'kind' => NodeKind::OBJECT_TYPE_DEFINITION, 'kind' => NodeKind::OBJECT_TYPE_DEFINITION,
'name' => $this->nameNode('Hello', $loc(5, 10)), 'name' => $this->nameNode('Hello', $loc(5, 10)),
'interfaces' => [ 'interfaces' => [
$this->typeNode('Wo', $loc(22,24)), $this->typeNode('Wo', $loc(22, 24)),
$this->typeNode('rld', $loc(26,29)) $this->typeNode('rld', $loc(27, 30))
], ],
'directives' => [], 'directives' => [],
'fields' => [ 'fields' => [
$this->fieldNode( $this->fieldNode(
$this->nameNode('field', $loc(32, 37)), $this->nameNode('field', $loc(33, 38)),
$this->typeNode('String', $loc(39, 45)), $this->typeNode('String', $loc(40, 46)),
$loc(32, 45) $loc(33, 46)
) )
], ],
'loc' => $loc(0, 47), 'loc' => $loc(0, 48),
'description' => null 'description' => null
] ]
], ],
'loc' => $loc(0, 47) 'loc' => $loc(0, 48)
]; ];
$this->assertEquals($expected, TestUtils::nodeToArray($doc)); $this->assertEquals($expected, TestUtils::nodeToArray($doc));
} }
/**
* @it Simple type inheriting multiple interfaces with leading ampersand
*/
public function testSimpleTypeInheritingMultipleInterfacesWithLeadingAmpersand()
{
$body = 'type Hello implements & Wo & rld { field: String }';
$loc = function($start, $end) {return TestUtils::locArray($start, $end);};
$doc = Parser::parse($body);
$expected = [
'kind' => 'Document',
'definitions' => [
[
'kind' => 'ObjectTypeDefinition',
'name' => $this->nameNode('Hello', $loc(5, 10)),
'interfaces' => [
$this->typeNode('Wo', $loc(24, 26)),
$this->typeNode('rld', $loc(29, 32)),
],
'directives' => [],
'fields' => [
$this->fieldNode(
$this->nameNode('field', $loc(35, 40)),
$this->typeNode('String', $loc(42, 48)),
$loc(35, 48)
),
],
'loc' => $loc(0, 50),
'description' => null,
],
],
'loc' => $loc(0, 50),
];
$this->assertEquals($expected, TestUtils::nodeToArray($doc));
}
/** /**
* @it Single value enum * @it Single value enum
*/ */
@ -884,6 +919,32 @@ input Hello {
$this->assertArraySubset($expected, $doc->toArray(true)); $this->assertArraySubset($expected, $doc->toArray(true));
} }
public function testDoesntAllowLegacySDLImplementsInterfacesByDefault()
{
$body = 'type Hello implements Wo rld { field: String }';
$this->expectSyntaxError($body, 'Syntax Error: Unexpected Name "rld"', new SourceLocation(1, 26));
}
/**
* @it Option: allowLegacySDLImplementsInterfaces
*/
public function testDefaultSDLImplementsInterfaces()
{
$body = 'type Hello implements Wo rld { field: String }';
$doc = Parser::parse($body, ['allowLegacySDLImplementsInterfaces' => true]);
$expected = [
'definitions' => [
[
'interfaces' => [
$this->typeNode('Wo', ['start' => 22, 'end' => 24]),
$this->typeNode('rld', ['start' => 25, 'end' => 28]),
],
],
],
];
$this->assertArraySubset($expected, $doc->toArray(true));
}
private function typeNode($name, $loc) private function typeNode($name, $loc)
{ {
return [ return [

View File

@ -64,7 +64,7 @@ class SchemaPrinterTest extends TestCase
This is a description This is a description
of the `Foo` type. of the `Foo` type.
""" """
type Foo implements Bar { type Foo implements Bar & Baz {
one: Type one: Type
two(argument: InputType!): Type two(argument: InputType!): Type
three(argument: InputType, other: String): Int three(argument: InputType, other: String): Int

View File

@ -12,7 +12,7 @@ schema {
This is a description This is a description
of the `Foo` type. of the `Foo` type.
""" """
type Foo implements Bar { type Foo implements Bar & Baz {
one: Type one: Type
two(argument: InputType!): Type two(argument: InputType!): Type
three(argument: InputType, other: String): Int three(argument: InputType, other: String): Int

View File

@ -1015,7 +1015,7 @@ class ValidationTest extends TestCase
field: String field: String
} }
type AnotherObject implements AnotherInterface, AnotherInterface { type AnotherObject implements AnotherInterface & AnotherInterface {
field: String field: String
} }
'); ');
@ -1023,7 +1023,7 @@ class ValidationTest extends TestCase
$schema->validate(), $schema->validate(),
[[ [[
'message' => 'Type AnotherObject can only implement AnotherInterface once.', '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]],
]] ]]
); );
} }