diff --git a/UPGRADE.md b/UPGRADE.md index 40b9d72..ac8947a 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -3,6 +3,22 @@ ### Breaking: minimum supported version of PHP 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 Extensions assigned to errors are shown under `extensions` key ```php diff --git a/src/Language/Lexer.php b/src/Language/Lexer.php index e39d933..835b947 100644 --- a/src/Language/Lexer.php +++ b/src/Language/Lexer.php @@ -147,6 +147,8 @@ class Lexer return $this->readComment($line, $col, $prev); case 36: // $ 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: // ( return new Token(Token::PAREN_L, $position, $position + 1, $line, $col, $prev); case 41: // ) diff --git a/src/Language/Parser.php b/src/Language/Parser.php index 98546f6..b920583 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -66,7 +66,6 @@ class Parser * in the source that they correspond to. This configuration flag * disables that behavior for performance or testing.) * - * * allowLegacySDLEmptyFields: boolean * If enabled, the parser will parse empty fields sets in the Schema * 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 * 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, * (If enabled, the parser will understand and parse variable definitions * contained in a fragment definition. They'll be represented in the @@ -1072,6 +1079,10 @@ class Parser } /** + * ImplementsInterfaces : + * - implements `&`? NamedType + * - ImplementsInterfaces & NamedType + * * @return NamedTypeNode[] */ function parseImplementsInterfaces() @@ -1079,9 +1090,15 @@ class Parser $types = []; if ($this->lexer->token->value === 'implements') { $this->lexer->advance(); + // Optional leading ampersand + $this->skip(Token::AMP); do { $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; } diff --git a/src/Language/Printer.php b/src/Language/Printer.php index ed25c66..ac76f14 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -212,7 +212,7 @@ class Printer $this->join([ 'type', $def->name, - $this->wrap('implements ', $this->join($def->interfaces, ', ')), + $this->wrap('implements ', $this->join($def->interfaces, ' & ')), $this->join($def->directives, ' '), $this->block($def->fields) ], ' ') @@ -300,7 +300,7 @@ class Printer return $this->join([ 'extend type', $def->name, - $this->wrap('implements ', $this->join($def->interfaces, ', ')), + $this->wrap('implements ', $this->join($def->interfaces, ' & ')), $this->join($def->directives, ' '), $this->block($def->fields), ], ' '); diff --git a/src/Language/Token.php b/src/Language/Token.php index 4b433d1..880cb0d 100644 --- a/src/Language/Token.php +++ b/src/Language/Token.php @@ -12,6 +12,7 @@ class Token const EOF = ''; const BANG = '!'; const DOLLAR = '$'; + const AMP = '&'; const PAREN_L = '('; const PAREN_R = ')'; const SPREAD = '...'; diff --git a/tests/Language/SchemaParserTest.php b/tests/Language/SchemaParserTest.php index a0a2b1f..dae4994 100644 --- a/tests/Language/SchemaParserTest.php +++ b/tests/Language/SchemaParserTest.php @@ -330,7 +330,7 @@ type Hello { */ 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);}; $doc = Parser::parse($body); @@ -341,27 +341,62 @@ type Hello { 'kind' => NodeKind::OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(5, 10)), 'interfaces' => [ - $this->typeNode('Wo', $loc(22,24)), - $this->typeNode('rld', $loc(26,29)) + $this->typeNode('Wo', $loc(22, 24)), + $this->typeNode('rld', $loc(27, 30)) ], 'directives' => [], 'fields' => [ $this->fieldNode( - $this->nameNode('field', $loc(32, 37)), - $this->typeNode('String', $loc(39, 45)), - $loc(32, 45) + $this->nameNode('field', $loc(33, 38)), + $this->typeNode('String', $loc(40, 46)), + $loc(33, 46) ) ], - 'loc' => $loc(0, 47), + 'loc' => $loc(0, 48), 'description' => null ] ], - 'loc' => $loc(0, 47) + 'loc' => $loc(0, 48) ]; $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 */ @@ -884,6 +919,32 @@ input Hello { $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) { return [ diff --git a/tests/Language/SchemaPrinterTest.php b/tests/Language/SchemaPrinterTest.php index c067d83..f81d410 100644 --- a/tests/Language/SchemaPrinterTest.php +++ b/tests/Language/SchemaPrinterTest.php @@ -64,7 +64,7 @@ class SchemaPrinterTest extends TestCase 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/tests/Language/schema-kitchen-sink.graphql b/tests/Language/schema-kitchen-sink.graphql index ae1a3e5..6ca3c59 100644 --- a/tests/Language/schema-kitchen-sink.graphql +++ b/tests/Language/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/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index 11c8aba..f5e2176 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -1015,7 +1015,7 @@ class ValidationTest extends TestCase field: String } - type AnotherObject implements AnotherInterface, AnotherInterface { + type AnotherObject implements AnotherInterface & AnotherInterface { field: String } '); @@ -1023,7 +1023,7 @@ class ValidationTest extends TestCase $schema->validate(), [[ '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]], ]] ); }