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 * @var string
*/ */
public $kind = self::UNION_TYPE_DEFINITION; public $kind = self::ENUM_TYPE_DEFINITION;
/** /**
* @var Name * @var Name

View File

@ -31,6 +31,7 @@ use GraphQL\Language\AST\ObjectField;
use GraphQL\Language\AST\ObjectTypeDefinition; use GraphQL\Language\AST\ObjectTypeDefinition;
use GraphQL\Language\AST\ObjectValue; use GraphQL\Language\AST\ObjectValue;
use GraphQL\Language\AST\OperationDefinition; use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Language\AST\OperationTypeDefinition;
use GraphQL\Language\AST\ScalarTypeDefinition; use GraphQL\Language\AST\ScalarTypeDefinition;
use GraphQL\Language\AST\SchemaDefinition; use GraphQL\Language\AST\SchemaDefinition;
use GraphQL\Language\AST\SelectionSet; 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 * @return ScalarTypeDefinition
* @throws SyntaxError * @throws SyntaxError

View File

@ -3,6 +3,13 @@ namespace GraphQL\Language;
use GraphQL\Language\AST\Argument; 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\ListValue;
use GraphQL\Language\AST\BooleanValue; use GraphQL\Language\AST\BooleanValue;
use GraphQL\Language\AST\Directive; use GraphQL\Language\AST\Directive;
@ -19,10 +26,16 @@ use GraphQL\Language\AST\NamedType;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NonNullType; use GraphQL\Language\AST\NonNullType;
use GraphQL\Language\AST\ObjectField; use GraphQL\Language\AST\ObjectField;
use GraphQL\Language\AST\ObjectTypeDefinition;
use GraphQL\Language\AST\ObjectValue; use GraphQL\Language\AST\ObjectValue;
use GraphQL\Language\AST\OperationDefinition; 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\SelectionSet;
use GraphQL\Language\AST\StringValue; use GraphQL\Language\AST\StringValue;
use GraphQL\Language\AST\TypeExtensionDefinition;
use GraphQL\Language\AST\UnionTypeDefinition;
use GraphQL\Language\AST\VariableDefinition; use GraphQL\Language\AST\VariableDefinition;
class Printer class Printer
@ -31,17 +44,20 @@ class Printer
{ {
return Visitor::visit($ast, array( return Visitor::visit($ast, array(
'leave' => 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::VARIABLE => function($node) {return '$' . $node->name;},
Node::DOCUMENT => function(Document $node) {return self::join($node->definitions, "\n\n") . "\n";}, Node::DOCUMENT => function(Document $node) {return self::join($node->definitions, "\n\n") . "\n";},
Node::OPERATION_DEFINITION => function(OperationDefinition $node) { Node::OPERATION_DEFINITION => function(OperationDefinition $node) {
$op = $node->operation; $op = $node->operation;
$name = $node->name; $name = $node->name;
$defs = self::wrap('(', self::join($node->variableDefinitions, ', '), ')'); $varDefs = self::wrap('(', self::join($node->variableDefinitions, ', '), ')');
$directives = self::join($node->directives, ' '); $directives = self::join($node->directives, ' ');
$selectionSet = $node->selectionSet; $selectionSet = $node->selectionSet;
return !$name ? $selectionSet : // Anonymous queries with no directives or variable definitions can use
self::join([$op, self::join([$name, $defs]), $directives, $selectionSet], ' '); // 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) { Node::VARIABLE_DEFINITION => function(VariableDefinition $node) {
return $node->variable . ': ' . $node->type . self::wrap(' = ', $node->defaultValue); return $node->variable . ': ' . $node->type . self::wrap(' = ', $node->defaultValue);
@ -55,25 +71,6 @@ class Printer
self::join($node->directives, ' '), self::join($node->directives, ' '),
$node->selectionSet $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) { Node::ARGUMENT => function(Argument $node) {
return $node->name . ': ' . $node->value; return $node->name . ': ' . $node->value;
@ -84,9 +81,12 @@ class Printer
return '...' . $node->name . self::wrap(' ', self::join($node->directives, ' ')); return '...' . $node->name . self::wrap(' ', self::join($node->directives, ' '));
}, },
Node::INLINE_FRAGMENT => function(InlineFragment $node) { Node::INLINE_FRAGMENT => function(InlineFragment $node) {
return "... on {$node->typeCondition} " return self::join([
. self::wrap('', self::join($node->directives, ' '), ' ') "...",
. $node->selectionSet; self::wrap('on ', $node->typeCondition),
self::join($node->directives, ' '),
$node->selectionSet
], ' ');
}, },
Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) { Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) {
return "fragment {$node->name} on {$node->typeCondition} " return "fragment {$node->name} on {$node->typeCondition} "
@ -112,7 +112,42 @@ class Printer
// Type // Type
Node::NAMED_TYPE => function(NamedType $node) {return $node->name;}, Node::NAMED_TYPE => function(NamedType $node) {return $node->name;},
Node::LIST_TYPE => function(ListType $node) {return '[' . $node->type . ']';}, 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) 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) public static function indent($maybeString)

View File

@ -65,6 +65,22 @@ class Visitor
Node::NAMED_TYPE => ['name'], Node::NAMED_TYPE => ['name'],
Node::LIST_TYPE => ['type'], Node::LIST_TYPE => ['type'],
Node::NON_NULL_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 class PrinterTest extends \PHPUnit_Framework_TestCase
{ {
/**
* @it does not alter ast
*/
public function testDoesntAlterAST() public function testDoesntAlterAST()
{ {
$kitchenSink = file_get_contents(__DIR__ . '/kitchen-sink.graphql'); $kitchenSink = file_get_contents(__DIR__ . '/kitchen-sink.graphql');
@ -27,12 +30,18 @@ class PrinterTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($astCopy, $ast); $this->assertEquals($astCopy, $ast);
} }
/**
* @it prints minimal ast
*/
public function testPrintsMinimalAst() public function testPrintsMinimalAst()
{ {
$ast = new Field(['name' => new Name(['value' => 'foo'])]); $ast = new Field(['name' => new Name(['value' => 'foo'])]);
$this->assertEquals('foo', Printer::doPrint($ast)); $this->assertEquals('foo', Printer::doPrint($ast));
} }
/**
* @it produces helpful error messages
*/
public function testProducesHelpfulErrorMessages() public function testProducesHelpfulErrorMessages()
{ {
$badAst1 = new \ArrayObject(array('random' => 'Data')); $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' $queryAstShorthanded = Parser::parse('query { id, name }');
query queryName($foo: ComplexType, $site: Site = MOBILE) {
whoever123is { $expected = '{
id id
} name
} }
';
$this->assertEquals($expected, Printer::doPrint($queryAstShorthanded));
EOT; $mutationAst = Parser::parse('mutation { id, name }');
; $expected = 'mutation {
$ast = Parser::parse($queryStr, ['noLocation' => true]); id
/* name
$expectedAst = new Document(array( }
'definitions' => [ ';
new OperationDefinition(array( $this->assertEquals($expected, Printer::doPrint($mutationAst));
'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); $queryAstWithArtifacts = Parser::parse(
$this->assertEquals($queryStr, Printer::doPrint($ast)); '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() public function testPrintsKitchenSink()
{ {
$kitchenSink = file_get_contents(__DIR__ . '/kitchen-sink.graphql'); $kitchenSink = file_get_contents(__DIR__ . '/kitchen-sink.graphql');
@ -117,16 +109,22 @@ EOT;
$expected = <<<'EOT' $expected = <<<'EOT'
query queryName($foo: ComplexType, $site: Site = MOBILE) { query queryName($foo: ComplexType, $site: Site = MOBILE) {
whoever123is: node(id: [123, 456]) { whoever123is: node(id: [123, 456]) {
id, id
... on User @defer { ... on User @defer {
field2 { field2 {
id, id
alias: field1(first: 10, after: $foo) @include(if: $foo) { alias: field1(first: 10, after: $foo) @include(if: $foo) {
id, id
...frag ...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 { fragment frag on Friend {
foo(size: $size, bar: $b, obj: {key: "value"}) foo(size: $size, bar: $b, obj: {key: "value"})
} }
{ {
unnamed(truthy: true, falsey: false), unnamed(truthy: true, falsey: false)
query 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