mirror of
https://github.com/retailcrm/graphql-php.git
synced 2025-02-06 07:49:24 +03:00
Schema language parsing / printing
This commit is contained in:
parent
4f4776726d
commit
687b023616
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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' ]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
97
tests/Language/SchemaPrinterTest.php
Normal file
97
tests/Language/SchemaPrinterTest.php
Normal 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);
|
||||
}
|
||||
}
|
50
tests/Language/schema-kitchen-sink.graphql
Normal file
50
tests/Language/schema-kitchen-sink.graphql
Normal 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
|
Loading…
x
Reference in New Issue
Block a user