From 687b023616acee3ac34b8a8a85e7317cbac7e87e Mon Sep 17 00:00:00 2001 From: vladar Date: Sun, 24 Apr 2016 17:01:04 +0600 Subject: [PATCH] Schema language parsing / printing --- src/Language/AST/EnumTypeDefinition.php | 2 +- src/Language/Parser.php | 15 +++ src/Language/Printer.php | 91 +++++++++----- src/Language/Visitor.php | 16 +++ tests/Language/PrinterTest.php | 131 +++++++++++---------- tests/Language/SchemaPrinterTest.php | 97 +++++++++++++++ tests/Language/schema-kitchen-sink.graphql | 50 ++++++++ 7 files changed, 313 insertions(+), 89 deletions(-) create mode 100644 tests/Language/SchemaPrinterTest.php create mode 100644 tests/Language/schema-kitchen-sink.graphql diff --git a/src/Language/AST/EnumTypeDefinition.php b/src/Language/AST/EnumTypeDefinition.php index d1c653e..6e91b85 100644 --- a/src/Language/AST/EnumTypeDefinition.php +++ b/src/Language/AST/EnumTypeDefinition.php @@ -6,7 +6,7 @@ class EnumTypeDefinition extends Node implements TypeDefinition /** * @var string */ - public $kind = self::UNION_TYPE_DEFINITION; + public $kind = self::ENUM_TYPE_DEFINITION; /** * @var Name diff --git a/src/Language/Parser.php b/src/Language/Parser.php index 2f7ffdd..636f0fb 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -31,6 +31,7 @@ use GraphQL\Language\AST\ObjectField; use GraphQL\Language\AST\ObjectTypeDefinition; use GraphQL\Language\AST\ObjectValue; use GraphQL\Language\AST\OperationDefinition; +use GraphQL\Language\AST\OperationTypeDefinition; use GraphQL\Language\AST\ScalarTypeDefinition; use GraphQL\Language\AST\SchemaDefinition; use GraphQL\Language\AST\SelectionSet; @@ -832,6 +833,20 @@ class Parser ]); } + function parseOperationTypeDefinition() + { + $start = $this->token->start; + $operation = $this->parseOperationType(); + $this->expect(Token::COLON); + $type = $this->parseNamedType(); + + return new OperationTypeDefinition([ + 'operation' => $operation, + 'type' => $type, + 'loc' => $this->loc($start) + ]); + } + /** * @return ScalarTypeDefinition * @throws SyntaxError diff --git a/src/Language/Printer.php b/src/Language/Printer.php index 155af55..b18e94e 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -3,6 +3,13 @@ namespace GraphQL\Language; use GraphQL\Language\AST\Argument; +use GraphQL\Language\AST\DirectiveDefinition; +use GraphQL\Language\AST\EnumTypeDefinition; +use GraphQL\Language\AST\EnumValueDefinition; +use GraphQL\Language\AST\FieldDefinition; +use GraphQL\Language\AST\InputObjectTypeDefinition; +use GraphQL\Language\AST\InputValueDefinition; +use GraphQL\Language\AST\InterfaceTypeDefinition; use GraphQL\Language\AST\ListValue; use GraphQL\Language\AST\BooleanValue; use GraphQL\Language\AST\Directive; @@ -19,10 +26,16 @@ use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NonNullType; use GraphQL\Language\AST\ObjectField; +use GraphQL\Language\AST\ObjectTypeDefinition; use GraphQL\Language\AST\ObjectValue; use GraphQL\Language\AST\OperationDefinition; +use GraphQL\Language\AST\OperationTypeDefinition; +use GraphQL\Language\AST\ScalarTypeDefinition; +use GraphQL\Language\AST\SchemaDefinition; use GraphQL\Language\AST\SelectionSet; use GraphQL\Language\AST\StringValue; +use GraphQL\Language\AST\TypeExtensionDefinition; +use GraphQL\Language\AST\UnionTypeDefinition; use GraphQL\Language\AST\VariableDefinition; class Printer @@ -31,17 +44,20 @@ class Printer { return Visitor::visit($ast, array( 'leave' => array( - Node::NAME => function($node) {return $node->value . '';}, + Node::NAME => function($node) {return '' . $node->value;}, Node::VARIABLE => function($node) {return '$' . $node->name;}, Node::DOCUMENT => function(Document $node) {return self::join($node->definitions, "\n\n") . "\n";}, Node::OPERATION_DEFINITION => function(OperationDefinition $node) { $op = $node->operation; $name = $node->name; - $defs = self::wrap('(', self::join($node->variableDefinitions, ', '), ')'); + $varDefs = self::wrap('(', self::join($node->variableDefinitions, ', '), ')'); $directives = self::join($node->directives, ' '); $selectionSet = $node->selectionSet; - return !$name ? $selectionSet : - self::join([$op, self::join([$name, $defs]), $directives, $selectionSet], ' '); + // Anonymous queries with no directives or variable definitions can use + // the query short form. + return !$name && !$directives && !$varDefs && $op === 'query' + ? $selectionSet + : self::join([$op, self::join([$name, $varDefs]), $directives, $selectionSet], ' '); }, Node::VARIABLE_DEFINITION => function(VariableDefinition $node) { return $node->variable . ': ' . $node->type . self::wrap(' = ', $node->defaultValue); @@ -55,25 +71,6 @@ class Printer self::join($node->directives, ' '), $node->selectionSet ], ' '); - /* - $r11 = self::join([ - $node->alias, - $node->name - ], ': '); - - $r1 = self::join([ - $r11, - self::manyList('(', $node->arguments, ', ', ')') - ]); - - $r2 = self::join($node->directives, ' '); - - return self::join([ - $r1, - $r2, - $node->selectionSet - ], ' '); - */ }, Node::ARGUMENT => function(Argument $node) { return $node->name . ': ' . $node->value; @@ -84,9 +81,12 @@ class Printer return '...' . $node->name . self::wrap(' ', self::join($node->directives, ' ')); }, Node::INLINE_FRAGMENT => function(InlineFragment $node) { - return "... on {$node->typeCondition} " - . self::wrap('', self::join($node->directives, ' '), ' ') - . $node->selectionSet; + return self::join([ + "...", + self::wrap('on ', $node->typeCondition), + self::join($node->directives, ' '), + $node->selectionSet + ], ' '); }, Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) { return "fragment {$node->name} on {$node->typeCondition} " @@ -112,7 +112,42 @@ class Printer // Type Node::NAMED_TYPE => function(NamedType $node) {return $node->name;}, Node::LIST_TYPE => function(ListType $node) {return '[' . $node->type . ']';}, - Node::NON_NULL_TYPE => function(NonNullType $node) {return $node->type . '!';} + Node::NON_NULL_TYPE => function(NonNullType $node) {return $node->type . '!';}, + + // Type System Definitions + Node::SCHEMA_DEFINITION => function(SchemaDefinition $def) {return 'schema ' . self::block($def->operationTypes);}, + Node::OPERATION_TYPE_DEFINITION => function(OperationTypeDefinition $def) {return $def->operation . ': ' . $def->type;}, + + Node::SCALAR_TYPE_DEFINITION => function(ScalarTypeDefinition $def) {return "scalar {$def->name}";}, + Node::OBJECT_TYPE_DEFINITION => function(ObjectTypeDefinition $def) { + return 'type ' . $def->name . ' ' . + self::wrap('implements ', self::join($def->interfaces, ', '), ' ') . + self::block($def->fields); + }, + Node::FIELD_DEFINITION => function(FieldDefinition $def) { + return $def->name . self::wrap('(', self::join($def->arguments, ', '), ')') . ': ' . $def->type; + }, + Node::INPUT_VALUE_DEFINITION => function(InputValueDefinition $def) { + return $def->name . ': ' . $def->type . self::wrap(' = ', $def->defaultValue); + }, + Node::INTERFACE_TYPE_DEFINITION => function(InterfaceTypeDefinition $def) { + return 'interface ' . $def->name . ' ' . self::block($def->fields); + }, + Node::UNION_TYPE_DEFINITION => function(UnionTypeDefinition $def) { + return 'union ' . $def->name . ' = ' . self::join($def->types, ' | '); + }, + Node::ENUM_TYPE_DEFINITION => function(EnumTypeDefinition $def) { + return 'enum ' . $def->name . ' ' . self::block($def->values); + }, + Node::ENUM_VALUE_DEFINITION => function(EnumValueDefinition $def) {return $def->name;}, + Node::INPUT_OBJECT_TYPE_DEFINITION => function(InputObjectTypeDefinition $def) { + return 'input ' . $def->name . ' ' . self::block($def->fields); + }, + Node::TYPE_EXTENSION_DEFINITION => function(TypeExtensionDefinition $def) {return "extend {$def->definition}";}, + Node::DIRECTIVE_DEFINITION => function(DirectiveDefinition $def) { + return 'directive @' . $def->name . self::wrap('(', self::join($def->arguments, ', '), ')') + . ' on ' . self::join($def->locations, ' | '); + } ) )); } @@ -132,7 +167,7 @@ class Printer */ public static function block($maybeArray) { - return self::length($maybeArray) ? self::indent("{\n" . self::join($maybeArray, ",\n")) . "\n}" : ''; + return self::length($maybeArray) ? self::indent("{\n" . self::join($maybeArray, "\n")) . "\n}" : ''; } public static function indent($maybeString) diff --git a/src/Language/Visitor.php b/src/Language/Visitor.php index a06e905..777c587 100644 --- a/src/Language/Visitor.php +++ b/src/Language/Visitor.php @@ -65,6 +65,22 @@ class Visitor Node::NAMED_TYPE => ['name'], Node::LIST_TYPE => ['type'], Node::NON_NULL_TYPE => ['type'], + + Node::SCHEMA_DEFINITION => ['operationTypes'], + Node::OPERATION_TYPE_DEFINITION => ['type'], + Node::SCALAR_TYPE_DEFINITION => ['name'], + Node::OBJECT_TYPE_DEFINITION => ['name', 'interfaces', 'fields'], + Node::FIELD_DEFINITION => ['name', 'arguments', 'type'], + Node::INPUT_VALUE_DEFINITION => ['name', 'type', 'defaultValue'], + Node::INPUT_VALUE_DEFINITION => [ 'name', 'type', 'defaultValue' ], + Node::INTERFACE_TYPE_DEFINITION => [ 'name', 'fields' ], + Node::UNION_TYPE_DEFINITION => [ 'name', 'types' ], + Node::ENUM_TYPE_DEFINITION => [ 'name', 'values' ], + + Node::ENUM_VALUE_DEFINITION => [ 'name' ], + Node::INPUT_OBJECT_TYPE_DEFINITION => [ 'name', 'fields' ], + Node::TYPE_EXTENSION_DEFINITION => [ 'definition' ], + Node::DIRECTIVE_DEFINITION => [ 'name', 'arguments', 'locations' ] ); /** diff --git a/tests/Language/PrinterTest.php b/tests/Language/PrinterTest.php index 2a24bb0..a73d55e 100644 --- a/tests/Language/PrinterTest.php +++ b/tests/Language/PrinterTest.php @@ -15,6 +15,9 @@ use GraphQL\Language\Printer; class PrinterTest extends \PHPUnit_Framework_TestCase { + /** + * @it does not alter ast + */ public function testDoesntAlterAST() { $kitchenSink = file_get_contents(__DIR__ . '/kitchen-sink.graphql'); @@ -27,12 +30,18 @@ class PrinterTest extends \PHPUnit_Framework_TestCase $this->assertEquals($astCopy, $ast); } + /** + * @it prints minimal ast + */ public function testPrintsMinimalAst() { $ast = new Field(['name' => new Name(['value' => 'foo'])]); $this->assertEquals('foo', Printer::doPrint($ast)); } + /** + * @it produces helpful error messages + */ public function testProducesHelpfulErrorMessages() { $badAst1 = new \ArrayObject(array('random' => 'Data')); @@ -44,69 +53,52 @@ class PrinterTest extends \PHPUnit_Framework_TestCase } } - public function testX() + /** + * @it correctly prints non-query operations without name + */ + public function testCorrectlyPrintsOpsWithoutName() { - $queryStr = <<<'EOT' -query queryName($foo: ComplexType, $site: Site = MOBILE) { - whoever123is { - id - } + $queryAstShorthanded = Parser::parse('query { id, name }'); + + $expected = '{ + id + name } +'; + $this->assertEquals($expected, Printer::doPrint($queryAstShorthanded)); -EOT; -; - $ast = Parser::parse($queryStr, ['noLocation' => true]); -/* - $expectedAst = new Document(array( - 'definitions' => [ - new OperationDefinition(array( - 'operation' => 'query', - 'name' => new Name([ - 'value' => 'queryName' - ]), - 'variableDefinitions' => [ - new VariableDefinition([ - 'variable' => new Variable([ - 'name' => new Name(['value' => 'foo']) - ]), - 'type' => new Name(['value' => 'ComplexType']) - ]), - new VariableDefinition([ - 'variable' => new Variable([ - 'name' => new Name(['value' => 'site']) - ]), - 'type' => new Name(['value' => 'Site']), - 'defaultValue' => new EnumValue(['value' => 'MOBILE']) - ]) - ], - 'directives' => [], - 'selectionSet' => new SelectionSet([ - 'selections' => [ - new Field([ - 'name' => new Name(['value' => 'whoever123is']), - 'arguments' => [], - 'directives' => [], - 'selectionSet' => new SelectionSet([ - 'selections' => [ - new Field([ - 'name' => new Name(['value' => 'id']), - 'arguments' => [], - 'directives' => [] - ]) - ] - ]) - ]) - ] - ]) - )) - ] - ));*/ + $mutationAst = Parser::parse('mutation { id, name }'); + $expected = 'mutation { + id + name +} +'; + $this->assertEquals($expected, Printer::doPrint($mutationAst)); - // $this->assertEquals($expectedAst, $ast); - $this->assertEquals($queryStr, Printer::doPrint($ast)); + $queryAstWithArtifacts = Parser::parse( + 'query ($foo: TestType) @testDirective { id, name }' + ); + $expected = 'query ($foo: TestType) @testDirective { + id + name +} +'; + $this->assertEquals($expected, Printer::doPrint($queryAstWithArtifacts)); + $mutationAstWithArtifacts = Parser::parse( + 'mutation ($foo: TestType) @testDirective { id, name }' + ); + $expected = 'mutation ($foo: TestType) @testDirective { + id + name +} +'; + $this->assertEquals($expected, Printer::doPrint($mutationAstWithArtifacts)); } + /** + * @it prints kitchen sink + */ public function testPrintsKitchenSink() { $kitchenSink = file_get_contents(__DIR__ . '/kitchen-sink.graphql'); @@ -117,16 +109,22 @@ EOT; $expected = <<<'EOT' query queryName($foo: ComplexType, $site: Site = MOBILE) { whoever123is: node(id: [123, 456]) { - id, + id ... on User @defer { field2 { - id, + id alias: field1(first: 10, after: $foo) @include(if: $foo) { - id, + id ...frag } } } + ... @skip(unless: $foo) { + id + } + ... { + id + } } } @@ -138,12 +136,25 @@ mutation likeStory { } } +subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) { + storyLikeSubscribe(input: $input) { + story { + likers { + count + } + likeSentence { + text + } + } + } +} + fragment frag on Friend { foo(size: $size, bar: $b, obj: {key: "value"}) } { - unnamed(truthy: true, falsey: false), + unnamed(truthy: true, falsey: false) query } diff --git a/tests/Language/SchemaPrinterTest.php b/tests/Language/SchemaPrinterTest.php new file mode 100644 index 0000000..792721c --- /dev/null +++ b/tests/Language/SchemaPrinterTest.php @@ -0,0 +1,97 @@ + new Name(['value' => 'foo']) + ]); + $this->assertEquals('scalar foo', Printer::doPrint($ast)); + } + + /** + * @it produces helpful error messages + */ + public function testProducesHelpfulErrorMessages() + { + // $badAst1 = { random: 'Data' }; + $badAst = (object) ['random' => 'Data']; + $this->setExpectedException('Exception', 'Invalid AST Node: {"random":"Data"}'); + Printer::doPrint($badAst); + } + + /** + * @it does not alter ast + */ + public function testDoesNotAlterAst() + { + $kitchenSink = file_get_contents(__DIR__ . '/schema-kitchen-sink.graphql'); + + $ast = Parser::parse($kitchenSink); + $astCopy = $ast->cloneDeep(); + Printer::doPrint($ast); + + $this->assertEquals($astCopy, $ast); + } + + public function testPrintsKitchenSink() + { + $kitchenSink = file_get_contents(__DIR__ . '/schema-kitchen-sink.graphql'); + + $ast = Parser::parse($kitchenSink); + $printed = Printer::doPrint($ast); + + $expected = 'schema { + query: QueryType + mutation: MutationType +} + +type Foo implements Bar { + one: Type + two(argument: InputType!): Type + three(argument: InputType, other: String): Int + four(argument: String = "string"): String + five(argument: [String] = ["string", "string"]): String + six(argument: InputType = {key: "value"}): Type +} + +interface Bar { + one: Type + four(argument: String = "string"): String +} + +union Feed = Story | Article | Advert + +scalar CustomScalar + +enum Site { + DESKTOP + MOBILE +} + +input InputType { + key: String! + answer: Int = 42 +} + +extend type Foo { + seven(argument: [String]): Type +} + +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +'; + $this->assertEquals($expected, $printed); + } +} diff --git a/tests/Language/schema-kitchen-sink.graphql b/tests/Language/schema-kitchen-sink.graphql new file mode 100644 index 0000000..e623ec4 --- /dev/null +++ b/tests/Language/schema-kitchen-sink.graphql @@ -0,0 +1,50 @@ +# 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 { + query: QueryType + mutation: MutationType +} + +type Foo implements Bar { + one: Type + two(argument: InputType!): Type + three(argument: InputType, other: String): Int + four(argument: String = "string"): String + five(argument: [String] = ["string", "string"]): String + six(argument: InputType = {key: "value"}): Type +} + +interface Bar { + one: Type + four(argument: String = "string"): String +} + +union Feed = Story | Article | Advert + +scalar CustomScalar + +enum Site { + DESKTOP + MOBILE +} + +input InputType { + key: String! + answer: Int = 42 +} + +extend type Foo { + seven(argument: [String]): Type +} + +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @include(if: Boolean!) + on FIELD + | FRAGMENT_SPREAD + | INLINE_FRAGMENT