Schema language parsing / printing

This commit is contained in:
vladar 2016-04-24 17:01:04 +06:00
parent 4f4776726d
commit 687b023616
7 changed files with 313 additions and 89 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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' ]
);
/**

View File

@ -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 {
$queryAstShorthanded = Parser::parse('query { id, name }');
$expected = '{
id
name
}
';
$this->assertEquals($expected, Printer::doPrint($queryAstShorthanded));
$mutationAst = Parser::parse('mutation { id, name }');
$expected = 'mutation {
id
name
}
';
$this->assertEquals($expected, Printer::doPrint($mutationAst));
$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));
}
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' => []
])
]
])
])
]
])
))
]
));*/
// $this->assertEquals($expectedAst, $ast);
$this->assertEquals($queryStr, Printer::doPrint($ast));
}
/**
* @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
}

View File

@ -0,0 +1,97 @@
<?php
namespace GraphQL\Tests;
use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\ScalarTypeDefinition;
use GraphQL\Language\Parser;
use GraphQL\Language\Printer;
class SchemaPrinterTest extends \PHPUnit_Framework_TestCase
{
/**
* @it prints minimal ast
*/
public function testPrintsMinimalAst()
{
$ast = new ScalarTypeDefinition([
'name' => 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);
}
}

View File

@ -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