Updated parser to 2016 spec version; schema language parsing

This commit is contained in:
vladar 2016-04-24 00:24:41 +06:00
parent 86adfde0a0
commit 4f4776726d
4 changed files with 1243 additions and 121 deletions

View File

@ -4,6 +4,13 @@ namespace GraphQL\Language;
// language/parser.js
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;
@ -21,10 +28,16 @@ use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\NamedType;
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\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\TypeSystemDefinition;
use GraphQL\Language\AST\UnionTypeDefinition;
use GraphQL\Language\AST\Variable;
use GraphQL\Language\AST\VariableDefinition;
use GraphQL\SyntaxError;
@ -48,7 +61,7 @@ class Parser
* @param array $options
* @return Document
*/
public static function parse($source, array $options = array())
public static function parse($source, array $options = [])
{
$sourceObj = $source instanceof Source ? $source : new Source($source);
$parser = new self($sourceObj, $options);
@ -80,7 +93,7 @@ class Parser
*/
private $token;
function __construct(Source $source, array $options = array())
function __construct(Source $source, array $options = [])
{
$this->lexer = new Lexer($source);
$this->source = $source;
@ -212,13 +225,13 @@ class Parser
* @param callable $parseFn
* @param int $closeKind
* @return array
* @throws Exception
* @throws SyntaxError
*/
function any($openKind, $parseFn, $closeKind)
{
$this->expect($openKind);
$nodes = array();
$nodes = [];
while (!$this->skip($closeKind)) {
$nodes[] = $parseFn($this);
}
@ -235,13 +248,13 @@ class Parser
* @param $parseFn
* @param $closeKind
* @return array
* @throws Exception
* @throws SyntaxError
*/
function many($openKind, $parseFn, $closeKind)
{
$this->expect($openKind);
$nodes = array($parseFn($this));
$nodes = [$parseFn($this)];
while (!$this->skip($closeKind)) {
$nodes[] = $parseFn($this);
}
@ -252,16 +265,16 @@ class Parser
* Converts a name lex token into a name parse node.
*
* @return Name
* @throws Exception
* @throws SyntaxError
*/
function parseName()
{
$token = $this->expect(Token::NAME);
return new Name(array(
return new Name([
'value' => $token->value,
'loc' => $this->loc($token->start)
));
]);
}
/**
@ -280,66 +293,113 @@ class Parser
* Implements the parsing rules in the Document section.
*
* @return Document
* @throws Exception
* @throws SyntaxError
*/
function parseDocument()
{
$start = $this->token->start;
$definitions = array();
$definitions = [];
do {
if ($this->peek(Token::BRACE_L)) {
$definitions[] = $this->parseOperationDefinition();
} else if ($this->peek(Token::NAME)) {
if ($this->token->value === 'query' || $this->token->value === 'mutation') {
$definitions[] = $this->parseOperationDefinition();
} else if ($this->token->value === 'fragment') {
$definitions[] = $this->parseFragmentDefinition();
} else {
throw $this->unexpected();
}
} else {
throw $this->unexpected();
}
$definitions[] = $this->parseDefinition();
} while (!$this->skip(Token::EOF));
return new Document(array(
return new Document([
'definitions' => $definitions,
'loc' => $this->loc($start)
));
]);
}
/**
* @return OperationDefinition|FragmentDefinition|TypeSystemDefinition
* @throws SyntaxError
*/
function parseDefinition()
{
if ($this->peek(Token::BRACE_L)) {
return $this->parseOperationDefinition();
}
if ($this->peek(Token::NAME)) {
switch ($this->token->value) {
case 'query':
case 'mutation':
// Note: subscription is an experimental non-spec addition.
case 'subscription':
return $this->parseOperationDefinition();
case 'fragment':
return $this->parseFragmentDefinition();
// Note: the Type System IDL is an experimental non-spec addition.
case 'schema':
case 'scalar':
case 'type':
case 'interface':
case 'union':
case 'enum':
case 'input':
case 'extend':
case 'directive':
return $this->parseTypeSystemDefinition();
}
}
throw $this->unexpected();
}
// Implements the parsing rules in the Operations section.
/**
* @return OperationDefinition
* @throws Exception
* @throws SyntaxError
*/
function parseOperationDefinition()
{
$start = $this->token->start;
if ($this->peek(Token::BRACE_L)) {
return new OperationDefinition(array(
return new OperationDefinition([
'operation' => 'query',
'name' => null,
'variableDefinitions' => null,
'directives' => array(),
'directives' => [],
'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start)
));
]);
}
$operationToken = $this->expect(Token::NAME);
$operation = $operationToken->value;
$operation = $this->parseOperationType();
return new OperationDefinition(array(
$name = null;
if ($this->peek(Token::NAME)) {
$name = $this->parseName();
}
return new OperationDefinition([
'operation' => $operation,
'name' => $this->parseName(),
'name' => $name,
'variableDefinitions' => $this->parseVariableDefinitions(),
'directives' => $this->parseDirectives(),
'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start)
));
]);
}
/**
* @return string
* @throws SyntaxError
*/
function parseOperationType()
{
$operationToken = $this->expect(Token::NAME);
switch ($operationToken->value) {
case 'query': return 'query';
case 'mutation': return 'mutation';
// Note: subscription is an experimental non-spec addition.
case 'subscription': return 'subscription';
}
throw $this->unexpected($operationToken);
}
/**
@ -350,15 +410,15 @@ class Parser
return $this->peek(Token::PAREN_L) ?
$this->many(
Token::PAREN_L,
array($this, 'parseVariableDefinition'),
[$this, 'parseVariableDefinition'],
Token::PAREN_R
) :
array();
[];
}
/**
* @return VariableDefinition
* @throws Exception
* @throws SyntaxError
*/
function parseVariableDefinition()
{
@ -368,44 +428,47 @@ class Parser
$this->expect(Token::COLON);
$type = $this->parseType();
return new VariableDefinition(array(
return new VariableDefinition([
'variable' => $var,
'type' => $type,
'defaultValue' =>
($this->skip(Token::EQUALS) ? $this->parseValueLiteral(true) : null),
'loc' => $this->loc($start)
));
]);
}
/**
* @return Variable
* @throws Exception
* @throws SyntaxError
*/
function parseVariable() {
function parseVariable()
{
$start = $this->token->start;
$this->expect(Token::DOLLAR);
return new Variable(array(
return new Variable([
'name' => $this->parseName(),
'loc' => $this->loc($start)
));
]);
}
/**
* @return SelectionSet
*/
function parseSelectionSet() {
function parseSelectionSet()
{
$start = $this->token->start;
return new SelectionSet(array(
'selections' => $this->many(Token::BRACE_L, array($this, 'parseSelection'), Token::BRACE_R),
return new SelectionSet([
'selections' => $this->many(Token::BRACE_L, [$this, 'parseSelection'], Token::BRACE_R),
'loc' => $this->loc($start)
));
]);
}
/**
* @return mixed
*/
function parseSelection() {
function parseSelection()
{
return $this->peek(Token::SPREAD) ?
$this->parseFragment() :
$this->parseField();
@ -414,7 +477,8 @@ class Parser
/**
* @return Field
*/
function parseField() {
function parseField()
{
$start = $this->token->start;
$nameOrAlias = $this->parseName();
@ -426,28 +490,29 @@ class Parser
$name = $nameOrAlias;
}
return new Field(array(
return new Field([
'alias' => $alias,
'name' => $name,
'arguments' => $this->parseArguments(),
'directives' => $this->parseDirectives(),
'selectionSet' => $this->peek(Token::BRACE_L) ? $this->parseSelectionSet() : null,
'loc' => $this->loc($start)
));
]);
}
/**
* @return array<Argument>
*/
function parseArguments() {
function parseArguments()
{
return $this->peek(Token::PAREN_L) ?
$this->many(Token::PAREN_L, array($this, 'parseArgument'), Token::PAREN_R) :
array();
$this->many(Token::PAREN_L, [$this, 'parseArgument'], Token::PAREN_R) :
[];
}
/**
* @return Argument
* @throws Exception
* @throws SyntaxError
*/
function parseArgument()
{
@ -457,44 +522,52 @@ class Parser
$this->expect(Token::COLON);
$value = $this->parseValueLiteral(false);
return new Argument(array(
return new Argument([
'name' => $name,
'value' => $value,
'loc' => $this->loc($start)
));
]);
}
// Implements the parsing rules in the Fragments section.
/**
* @return FragmentSpread|InlineFragment
* @throws Exception
* @throws SyntaxError
*/
function parseFragment() {
function parseFragment()
{
$start = $this->token->start;
$this->expect(Token::SPREAD);
if ($this->token->value === 'on') {
$this->advance();
return new InlineFragment(array(
'typeCondition' => $this->parseNamedType(),
'directives' => $this->parseDirectives(),
'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start)
));
}
return new FragmentSpread(array(
if ($this->peek(Token::NAME) && $this->token->value !== 'on') {
return new FragmentSpread([
'name' => $this->parseFragmentName(),
'directives' => $this->parseDirectives(),
'loc' => $this->loc($start)
));
]);
}
$typeCondition = null;
if ($this->token->value === 'on') {
$this->advance();
$typeCondition = $this->parseNamedType();
}
return new InlineFragment([
'typeCondition' => $typeCondition,
'directives' => $this->parseDirectives(),
'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start)
]);
}
/**
* @return FragmentDefinition
* @throws SyntaxError
*/
function parseFragmentDefinition() {
function parseFragmentDefinition()
{
$start = $this->token->start;
$this->expectKeyword('fragment');
@ -502,13 +575,13 @@ class Parser
$this->expectKeyword('on');
$typeCondition = $this->parseNamedType();
return new FragmentDefinition(array(
return new FragmentDefinition([
'name' => $name,
'typeCondition' => $typeCondition,
'directives' => $this->parseDirectives(),
'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start)
));
]);
}
// Implements the parsing rules in the Values section.
@ -519,7 +592,7 @@ class Parser
/**
* @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable
* @throws Exception
* @throws SyntaxError
*/
function parseConstValue()
{
@ -531,7 +604,8 @@ class Parser
* @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable
* @throws SyntaxError
*/
function parseValueLiteral($isConst) {
function parseValueLiteral($isConst)
{
$token = $this->token;
switch ($token->kind) {
case Token::BRACKET_L:
@ -540,35 +614,35 @@ class Parser
return $this->parseObject($isConst);
case Token::INT:
$this->advance();
return new IntValue(array(
return new IntValue([
'value' => $token->value,
'loc' => $this->loc($token->start)
));
]);
case Token::FLOAT:
$this->advance();
return new FloatValue(array(
return new FloatValue([
'value' => $token->value,
'loc' => $this->loc($token->start)
));
]);
case Token::STRING:
$this->advance();
return new StringValue(array(
return new StringValue([
'value' => $token->value,
'loc' => $this->loc($token->start)
));
]);
case Token::NAME:
if ($token->value === 'true' || $token->value === 'false') {
$this->advance();
return new BooleanValue(array(
return new BooleanValue([
'value' => $token->value === 'true',
'loc' => $this->loc($token->start)
));
]);
} else if ($token->value !== 'null') {
$this->advance();
return new EnumValue(array(
return new EnumValue([
'value' => $token->value,
'loc' => $this->loc($token->start)
));
]);
}
break;
@ -589,25 +663,25 @@ class Parser
{
$start = $this->token->start;
$item = $isConst ? 'parseConstValue' : 'parseVariableValue';
return new ListValue(array(
'values' => $this->any(Token::BRACKET_L, array($this, $item), Token::BRACKET_R),
return new ListValue([
'values' => $this->any(Token::BRACKET_L, [$this, $item], Token::BRACKET_R),
'loc' => $this->loc($start)
));
]);
}
function parseObject($isConst)
{
$start = $this->token->start;
$this->expect(Token::BRACE_L);
$fieldNames = array();
$fields = array();
$fieldNames = [];
$fields = [];
while (!$this->skip(Token::BRACE_R)) {
$fields[] = $this->parseObjectField($isConst, $fieldNames);
}
return new ObjectValue(array(
return new ObjectValue([
'fields' => $fields,
'loc' => $this->loc($start)
));
]);
}
function parseObjectField($isConst, &$fieldNames)
@ -621,11 +695,11 @@ class Parser
$fieldNames[$name->value] = true;
$this->expect(Token::COLON);
return new ObjectField(array(
return new ObjectField([
'name' => $name,
'value' => $this->parseValueLiteral($isConst),
'loc' => $this->loc($start)
));
]);
}
// Implements the parsing rules in the Directives section.
@ -635,7 +709,7 @@ class Parser
*/
function parseDirectives()
{
$directives = array();
$directives = [];
while ($this->peek(Token::AT)) {
$directives[] = $this->parseDirective();
}
@ -644,17 +718,17 @@ class Parser
/**
* @return Directive
* @throws Exception
* @throws SyntaxError
*/
function parseDirective()
{
$start = $this->token->start;
$this->expect(Token::AT);
return new Directive(array(
return new Directive([
'name' => $this->parseName(),
'arguments' => $this->parseArguments(),
'loc' => $this->loc($start)
));
]);
}
// Implements the parsing rules in the Types section.
@ -672,18 +746,18 @@ class Parser
if ($this->skip(Token::BRACKET_L)) {
$type = $this->parseType();
$this->expect(Token::BRACKET_R);
$type = new ListType(array(
$type = new ListType([
'type' => $type,
'loc' => $this->loc($start)
));
]);
} else {
$type = $this->parseNamedType();
}
if ($this->skip(Token::BANG)) {
return new NonNullType(array(
return new NonNullType([
'type' => $type,
'loc' => $this->loc($start)
));
]);
}
return $type;
@ -697,6 +771,333 @@ class Parser
'name' => $this->parseName(),
'loc' => $this->loc($start)
]);
}
// Implements the parsing rules in the Type Definition section.
/**
* TypeSystemDefinition :
* - TypeDefinition
* - TypeExtensionDefinition
* - DirectiveDefinition
*
* TypeDefinition :
* - ScalarTypeDefinition
* - ObjectTypeDefinition
* - InterfaceTypeDefinition
* - UnionTypeDefinition
* - EnumTypeDefinition
* - InputObjectTypeDefinition
*
* @return TypeSystemDefinition
* @throws SyntaxError
*/
function parseTypeSystemDefinition()
{
if ($this->peek(Token::NAME)) {
switch ($this->token->value) {
case 'schema': return $this->parseSchemaDefinition();
case 'scalar': return $this->parseScalarTypeDefinition();
case 'type': return $this->parseObjectTypeDefinition();
case 'interface': return $this->parseInterfaceTypeDefinition();
case 'union': return $this->parseUnionTypeDefinition();
case 'enum': return $this->parseEnumTypeDefinition();
case 'input': return $this->parseInputObjectTypeDefinition();
case 'extend': return $this->parseTypeExtensionDefinition();
case 'directive': return $this->parseDirectiveDefinition();
}
}
throw $this->unexpected();
}
/**
* @return SchemaDefinition
* @throws SyntaxError
*/
function parseSchemaDefinition()
{
$start = $this->token->start;
$this->expectKeyword('schema');
$operationTypes = $this->many(
Token::BRACE_L,
[$this, 'parseOperationTypeDefinition'],
Token::BRACE_R
);
return new SchemaDefinition([
'operationTypes' => $operationTypes,
'loc' => $this->loc($start)
]);
}
/**
* @return ScalarTypeDefinition
* @throws SyntaxError
*/
function parseScalarTypeDefinition()
{
$start = $this->token->start;
$this->expectKeyword('scalar');
$name = $this->parseName();
return new ScalarTypeDefinition([
'name' => $name,
'loc' => $this->loc($start)
]);
}
/**
* @return ObjectTypeDefinition
* @throws SyntaxError
*/
function parseObjectTypeDefinition()
{
$start = $this->token->start;
$this->expectKeyword('type');
$name = $this->parseName();
$interfaces = $this->parseImplementsInterfaces();
$fields = $this->any(
Token::BRACE_L,
[$this, 'parseFieldDefinition'],
Token::BRACE_R
);
return new ObjectTypeDefinition([
'name' => $name,
'interfaces' => $interfaces,
'fields' => $fields,
'loc' => $this->loc($start)
]);
}
/**
* @return NamedType[]
*/
function parseImplementsInterfaces()
{
$types = [];
if ($this->token->value === 'implements') {
$this->advance();
do {
$types[] = $this->parseNamedType();
} while (!$this->peek(Token::BRACE_L));
}
return $types;
}
/**
* @return FieldDefinition
* @throws SyntaxError
*/
function parseFieldDefinition()
{
$start = $this->token->start;
$name = $this->parseName();
$args = $this->parseArgumentDefs();
$this->expect(Token::COLON);
$type = $this->parseType();
return new FieldDefinition([
'name' => $name,
'arguments' => $args,
'type' => $type,
'loc' => $this->loc($start)
]);
}
/**
* @return InputValueDefinition[]
*/
function parseArgumentDefs()
{
if (!$this->peek(Token::PAREN_L)) {
return [];
}
return $this->many(Token::PAREN_L, [$this, 'parseInputValueDef'], Token::PAREN_R);
}
/**
* @return InputValueDefinition
* @throws SyntaxError
*/
function parseInputValueDef()
{
$start = $this->token->start;
$name = $this->parseName();
$this->expect(Token::COLON);
$type = $this->parseType();
$defaultValue = null;
if ($this->skip(Token::EQUALS)) {
$defaultValue = $this->parseConstValue();
}
return new InputValueDefinition([
'name' => $name,
'type' => $type,
'defaultValue' => $defaultValue,
'loc' => $this->loc($start)
]);
}
/**
* @return InterfaceTypeDefinition
* @throws SyntaxError
*/
function parseInterfaceTypeDefinition()
{
$start = $this->token->start;
$this->expectKeyword('interface');
$name = $this->parseName();
$fields = $this->any(
Token::BRACE_L,
[$this, 'parseFieldDefinition'],
Token::BRACE_R
);
return new InterfaceTypeDefinition([
'name' => $name,
'fields' => $fields,
'loc' => $this->loc($start)
]);
}
/**
* @return UnionTypeDefinition
* @throws SyntaxError
*/
function parseUnionTypeDefinition()
{
$start = $this->token->start;
$this->expectKeyword('union');
$name = $this->parseName();
$this->expect(Token::EQUALS);
$types = $this->parseUnionMembers();
return new UnionTypeDefinition([
'name' => $name,
'types' => $types,
'loc' => $this->loc($start)
]);
}
/**
* @return NamedType[]
*/
function parseUnionMembers()
{
$members = [];
do {
$members[] = $this->parseNamedType();
} while ($this->skip(Token::PIPE));
return $members;
}
/**
* @return EnumTypeDefinition
* @throws SyntaxError
*/
function parseEnumTypeDefinition()
{
$start = $this->token->start;
$this->expectKeyword('enum');
$name = $this->parseName();
$values = $this->many(
Token::BRACE_L,
[$this, 'parseEnumValueDefinition'],
Token::BRACE_R
);
return new EnumTypeDefinition([
'name' => $name,
'values' => $values,
'loc' => $this->loc($start)
]);
}
/**
* @return EnumValueDefinition
*/
function parseEnumValueDefinition()
{
$start = $this->token->start;
$name = $this->parseName();
return new EnumValueDefinition([
'name' => $name,
'loc' => $this->loc($start)
]);
}
/**
* @return InputObjectTypeDefinition
* @throws SyntaxError
*/
function parseInputObjectTypeDefinition()
{
$start = $this->token->start;
$this->expectKeyword('input');
$name = $this->parseName();
$fields = $this->any(
Token::BRACE_L,
[$this, 'parseInputValueDef'],
Token::BRACE_R
);
return new InputObjectTypeDefinition([
'name' => $name,
'fields' => $fields,
'loc' => $this->loc($start)
]);
}
/**
* @return TypeExtensionDefinition
* @throws SyntaxError
*/
function parseTypeExtensionDefinition()
{
$start = $this->token->start;
$this->expectKeyword('extend');
$definition = $this->parseObjectTypeDefinition();
return new TypeExtensionDefinition([
'definition' => $definition,
'loc' => $this->loc($start)
]);
}
/**
* @return DirectiveDefinition
* @throws SyntaxError
*/
function parseDirectiveDefinition()
{
$start = $this->token->start;
$this->expectKeyword('directive');
$this->expect(Token::AT);
$name = $this->parseName();
$args = $this->parseArgumentDefs();
$this->expectKeyword('on');
$locations = $this->parseDirectiveLocations();
return new DirectiveDefinition([
'name' => $name,
'arguments' => $args,
'locations' => $locations,
'loc' => $this->loc($start)
]);
}
/**
* @return Name[]
*/
function parseDirectiveLocations()
{
$locations = [];
do {
$locations[] = $this->parseName();
} while ($this->skip(Token::PIPE));
return $locations;
}
}

View File

@ -1,7 +1,6 @@
<?php
namespace GraphQL\Tests\Language;
use GraphQL\Error;
use GraphQL\Language\AST\Argument;
use GraphQL\Language\AST\Document;
use GraphQL\Language\AST\Field;
@ -10,16 +9,20 @@ use GraphQL\Language\AST\Location;
use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Language\AST\SelectionSet;
use GraphQL\Language\AST\StringValue;
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
use GraphQL\SyntaxError;
use GraphQL\Utils;
class ParserTest extends \PHPUnit_Framework_TestCase
{
/**
* @it accepts option to not include source
*/
public function testAcceptsOptionToNotIncludeSource()
{
// accepts option to not include source
$actual = Parser::parse('{ field }', ['noSource' => true]);
$expected = new Document([
@ -54,6 +57,9 @@ class ParserTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, $actual);
}
/**
* @it parse provides useful errors
*/
public function testParseProvidesUsefulErrors()
{
$run = function($num, $str, $expectedMessage, $expectedPositions = null, $expectedLocations = null) {
@ -72,6 +78,7 @@ class ParserTest extends \PHPUnit_Framework_TestCase
}
};
$run(0, '{', "Syntax Error GraphQL (1:2) Expected Name, found EOF\n\n1: {\n ^\n", [1], [new SourceLocation(1,2)]);
$run(1,
'{ ...MissingOn }
fragment MissingOn Type
@ -82,25 +89,33 @@ fragment MissingOn Type
$run(2, '{ field: {} }', "Syntax Error GraphQL (1:10) Expected Name, found {\n\n1: { field: {} }\n ^\n");
$run(3, 'notanoperation Foo { field }', "Syntax Error GraphQL (1:1) Unexpected Name \"notanoperation\"\n\n1: notanoperation Foo { field }\n ^\n");
$run(4, '...', "Syntax Error GraphQL (1:1) Unexpected ...\n\n1: ...\n ^\n");
$run(5, '{', "Syntax Error GraphQL (1:2) Expected Name, found EOF\n\n1: {\n ^\n", [1], [new SourceLocation(1,2)]);
}
/**
* @it parse provides useful error when using source
*/
public function testParseProvidesUsefulErrorWhenUsingSource()
{
try {
Parser::parse(new Source('query', 'MyQuery.graphql'));
$this->fail('Expected exception not thrown');
} catch (SyntaxError $e) {
$this->assertEquals("Syntax Error MyQuery.graphql (1:6) Expected Name, found EOF\n\n1: query\n ^\n", $e->getMessage());
$this->assertEquals("Syntax Error MyQuery.graphql (1:6) Expected {, found EOF\n\n1: query\n ^\n", $e->getMessage());
}
}
/**
* @it parses variable inline values
*/
public function testParsesVariableInlineValues()
{
// Following line should not throw:
Parser::parse('{ field(complex: { a: { b: [ $var ] } }) }');
}
/**
* @it parses constant default values
*/
public function testParsesConstantDefaultValues()
{
try {
@ -114,39 +129,71 @@ fragment MissingOn Type
}
}
public function testDuplicateKeysInInputObjectIsSyntaxError()
{
try {
Parser::parse('{ field(arg: { a: 1, a: 2 }) }');
$this->fail('Expected exception not thrown');
} catch (SyntaxError $e) {
$this->assertEquals(
"Syntax Error GraphQL (1:22) Duplicate input object field a.\n\n1: { field(arg: { a: 1, a: 2 }) }\n ^\n",
$e->getMessage()
);
}
}
/**
* @it does not accept fragments spread of "on"
*/
public function testDoesNotAcceptFragmentsNamedOn()
{
// does not accept fragments named "on"
$this->setExpectedException('GraphQL\SyntaxError', 'Syntax Error GraphQL (1:10) Unexpected Name "on"');
Parser::parse('fragment on on on { on }');
}
/**
* @it does not accept fragments spread of "on"
*/
public function testDoesNotAcceptFragmentSpreadOfOn()
{
// does not accept fragments spread of "on"
$this->setExpectedException('GraphQL\SyntaxError', 'Syntax Error GraphQL (1:9) Expected Name, found }');
Parser::parse('{ ...on }');
}
/**
* @it does not allow null as value
*/
public function testDoesNotAllowNullAsValue()
{
$this->setExpectedException('GraphQL\SyntaxError', 'Syntax Error GraphQL (1:39) Unexpected Name "null"');
Parser::parse('{ fieldWithNullableStringInput(input: null) }');
}
/**
* @it parses multi-byte characters
*/
public function testParsesMultiByteCharacters()
{
// Note: \u0A0A could be naively interpretted as two line-feed chars.
$char = Utils::chr(0x0A0A);
$query = <<<HEREDOC
# This comment has a $char multi-byte character.
{ field(arg: "Has a $char multi-byte character.") }
HEREDOC;
$result = Parser::parse($query, ['noLocation' => true]);
$expected = new SelectionSet([
'selections' => [
new Field([
'name' => new Name(['value' => 'field']),
'arguments' => [
new Argument([
'name' => new Name(['value' => 'arg']),
'value' => new StringValue([
'value' => "Has a $char multi-byte character."
])
])
],
'directives' => []
])
]
]);
$this->assertEquals($expected, $result->definitions[0]->selectionSet);
}
/**
* @it parses kitchen sink
*/
public function testParsesKitchenSink()
{
// Following should not throw:
@ -155,14 +202,17 @@ fragment MissingOn Type
$this->assertNotEmpty($result);
}
/**
* allows non-keywords anywhere a Name is allowed
*/
public function testAllowsNonKeywordsAnywhereANameIsAllowed()
{
// allows non-keywords anywhere a Name is allowed
$nonKeywords = [
'on',
'fragment',
'query',
'mutation',
'subscription',
'true',
'false'
];
@ -185,6 +235,60 @@ fragment $fragmentName on Type {
}
}
/**
* @it parses anonymous mutation operations
*/
public function testParsessAnonymousMutationOperations()
{
// Should not throw:
Parser::parse('
mutation {
mutationField
}
');
}
/**
* @it parses anonymous subscription operations
*/
public function testParsesAnonymousSubscriptionOperations()
{
// Should not throw:
Parser::parse('
subscription {
subscriptionField
}
');
}
/**
* @it parses named mutation operations
*/
public function testParsesNamedMutationOperations()
{
// Should not throw:
Parser::parse('
mutation Foo {
mutationField
}
');
}
/**
* @it parses named subscription operations
*/
public function testParsesNamedSubscriptionOperations()
{
Parser::parse('
subscription Foo {
subscriptionField
}
');
}
/**
* @it parse creates ast
*/
public function testParseCreatesAst()
{
$source = new Source('{

View File

@ -0,0 +1,598 @@
<?php
namespace GraphQL\Tests\Language;
use GraphQL\Language\AST\BooleanValue;
use GraphQL\Language\AST\Document;
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\ListType;
use GraphQL\Language\AST\Location;
use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\NamedType;
use GraphQL\Language\AST\NonNullType;
use GraphQL\Language\AST\ObjectTypeDefinition;
use GraphQL\Language\AST\ScalarTypeDefinition;
use GraphQL\Language\AST\TypeExtensionDefinition;
use GraphQL\Language\AST\UnionTypeDefinition;
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
class SchemaParserTest extends \PHPUnit_Framework_TestCase
{
/**
* @it Simple type
*/
public function testSimpleType()
{
$body = '
type Hello {
world: String
}';
$doc = Parser::parse($body);
$loc = $this->createLocFn($body);
$expected = new Document([
'definitions' => [
new ObjectTypeDefinition([
'name' => $this->nameNode('Hello', $loc(6, 11)),
'interfaces' => [],
'fields' => [
$this->fieldNode(
$this->nameNode('world', $loc(16, 21)),
$this->typeNode('String', $loc(23, 29)),
$loc(16, 29)
)
],
'loc' => $loc(1, 31)
])
],
'loc' => $loc(1, 31)
]);
$this->assertEquals($doc, $expected);
}
/**
* @it Simple extension
*/
public function testSimpleExtension()
{
$body = '
extend type Hello {
world: String
}';
$doc = Parser::parse($body);
$loc = $this->createLocFn($body);
$expected = new Document([
'definitions' => [
new TypeExtensionDefinition([
'definition' => new ObjectTypeDefinition([
'name' => $this->nameNode('Hello', $loc(13, 18)),
'interfaces' => [],
'fields' => [
$this->fieldNode(
$this->nameNode('world', $loc(23, 28)),
$this->typeNode('String', $loc(30, 36)),
$loc(23, 36)
)
],
'loc' => $loc(8, 38)
]),
'loc' => $loc(1, 38)
])
],
'loc' => $loc(1, 38)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Simple non-null type
*/
public function testSimpleNonNullType()
{
$body = '
type Hello {
world: String!
}';
$loc = $this->createLocFn($body);
$doc = Parser::parse($body);
$expected = new Document([
'definitions' => [
new ObjectTypeDefinition([
'name' => $this->nameNode('Hello', $loc(6,11)),
'interfaces' => [],
'fields' => [
$this->fieldNode(
$this->nameNode('world', $loc(16, 21)),
new NonNullType([
'type' => $this->typeNode('String', $loc(23, 29)),
'loc' => $loc(23, 30)
]),
$loc(16,30)
)
],
'loc' => $loc(1,32)
])
],
'loc' => $loc(1,32)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Simple type inheriting interface
*/
public function testSimpleTypeInheritingInterface()
{
$body = 'type Hello implements World { }';
$loc = $this->createLocFn($body);
$doc = Parser::parse($body);
$expected = new Document([
'definitions' => [
new ObjectTypeDefinition([
'name' => $this->nameNode('Hello', $loc(5, 10)),
'interfaces' => [
$this->typeNode('World', $loc(22, 27))
],
'fields' => [],
'loc' => $loc(0,31)
])
],
'loc' => $loc(0,31)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Simple type inheriting multiple interfaces
*/
public function testSimpleTypeInheritingMultipleInterfaces()
{
$body = 'type Hello implements Wo, rld { }';
$loc = $this->createLocFn($body);
$doc = Parser::parse($body);
$expected = new Document([
'definitions' => [
new ObjectTypeDefinition([
'name' => $this->nameNode('Hello', $loc(5, 10)),
'interfaces' => [
$this->typeNode('Wo', $loc(22,24)),
$this->typeNode('rld', $loc(26,29))
],
'fields' => [],
'loc' => $loc(0, 33)
])
],
'loc' => $loc(0, 33)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Single value enum
*/
public function testSingleValueEnum()
{
$body = 'enum Hello { WORLD }';
$loc = $this->createLocFn($body);
$doc = Parser::parse($body);
$expected = new Document([
'definitions' => [
new EnumTypeDefinition([
'name' => $this->nameNode('Hello', $loc(5, 10)),
'values' => [$this->enumValueNode('WORLD', $loc(13, 18))],
'loc' => $loc(0, 20)
])
],
'loc' => $loc(0, 20)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Double value enum
*/
public function testDoubleValueEnum()
{
$body = 'enum Hello { WO, RLD }';
$loc = $this->createLocFn($body);
$doc = Parser::parse($body);
$expected = new Document([
'definitions' => [
new EnumTypeDefinition([
'name' => $this->nameNode('Hello', $loc(5, 10)),
'values' => [
$this->enumValueNode('WO', $loc(13, 15)),
$this->enumValueNode('RLD', $loc(17, 20))
],
'loc' => $loc(0, 22)
])
],
'loc' => $loc(0, 22)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Simple interface
*/
public function testSimpleInterface()
{
$body = '
interface Hello {
world: String
}';
$doc = Parser::parse($body);
$loc = $this->createLocFn($body);
$expected = new Document([
'definitions' => [
new InterfaceTypeDefinition([
'name' => $this->nameNode('Hello', $loc(11, 16)),
'fields' => [
$this->fieldNode(
$this->nameNode('world', $loc(21, 26)),
$this->typeNode('String', $loc(28, 34)),
$loc(21, 34)
)
],
'loc' => $loc(1, 36)
])
],
'loc' => $loc(1,36)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Simple field with arg
*/
public function testSimpleFieldWithArg()
{
$body = '
type Hello {
world(flag: Boolean): String
}';
$doc = Parser::parse($body);
$loc = $this->createLocFn($body);
$expected = new Document([
'definitions' => [
new ObjectTypeDefinition([
'name' => $this->nameNode('Hello', $loc(6, 11)),
'interfaces' => [],
'fields' => [
$this->fieldNodeWithArgs(
$this->nameNode('world', $loc(16, 21)),
$this->typeNode('String', $loc(38, 44)),
[
$this->inputValueNode(
$this->nameNode('flag', $loc(22, 26)),
$this->typeNode('Boolean', $loc(28, 35)),
null,
$loc(22, 35)
)
],
$loc(16, 44)
)
],
'loc' => $loc(1, 46)
])
],
'loc' => $loc(1, 46)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Simple field with arg with default value
*/
public function testSimpleFieldWithArgWithDefaultValue()
{
$body = '
type Hello {
world(flag: Boolean = true): String
}';
$doc = Parser::parse($body);
$loc = $this->createLocFn($body);
$expected = new Document([
'definitions' => [
new ObjectTypeDefinition([
'name' => $this->nameNode('Hello', $loc(6, 11)),
'interfaces' => [],
'fields' => [
$this->fieldNodeWithArgs(
$this->nameNode('world', $loc(16, 21)),
$this->typeNode('String', $loc(45, 51)),
[
$this->inputValueNode(
$this->nameNode('flag', $loc(22, 26)),
$this->typeNode('Boolean', $loc(28, 35)),
new BooleanValue(['value' => true, 'loc' => $loc(38, 42)]),
$loc(22, 42)
)
],
$loc(16, 51)
)
],
'loc' => $loc(1, 53)
])
],
'loc' => $loc(1, 53)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Simple field with list arg
*/
public function testSimpleFieldWithListArg()
{
$body = '
type Hello {
world(things: [String]): String
}';
$doc = Parser::parse($body);
$loc = $this->createLocFn($body);
$expected = new Document([
'definitions' => [
new ObjectTypeDefinition([
'name' => $this->nameNode('Hello', $loc(6, 11)),
'interfaces' => [],
'fields' => [
$this->fieldNodeWithArgs(
$this->nameNode('world', $loc(16, 21)),
$this->typeNode('String', $loc(41, 47)),
[
$this->inputValueNode(
$this->nameNode('things', $loc(22,28)),
new ListType(['type' => $this->typeNode('String', $loc(31, 37)), 'loc' => $loc(30, 38)]),
null,
$loc(22, 38)
)
],
$loc(16, 47)
)
],
'loc' => $loc(1, 49)
])
],
'loc' => $loc(1, 49)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Simple field with two args
*/
public function testSimpleFieldWithTwoArgs()
{
$body = '
type Hello {
world(argOne: Boolean, argTwo: Int): String
}';
$doc = Parser::parse($body);
$loc = $this->createLocFn($body);
$expected = new Document([
'definitions' => [
new ObjectTypeDefinition([
'name' => $this->nameNode('Hello', $loc(6, 11)),
'interfaces' => [],
'fields' => [
$this->fieldNodeWithArgs(
$this->nameNode('world', $loc(16, 21)),
$this->typeNode('String', $loc(53, 59)),
[
$this->inputValueNode(
$this->nameNode('argOne', $loc(22, 28)),
$this->typeNode('Boolean', $loc(30, 37)),
null,
$loc(22, 37)
),
$this->inputValueNode(
$this->nameNode('argTwo', $loc(39, 45)),
$this->typeNode('Int', $loc(47, 50)),
null,
$loc(39, 50)
)
],
$loc(16, 59)
)
],
'loc' => $loc(1, 61)
])
],
'loc' => $loc(1, 61)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Simple union
*/
public function testSimpleUnion()
{
$body = 'union Hello = World';
$doc = Parser::parse($body);
$loc = $this->createLocFn($body);
$expected = new Document([
'definitions' => [
new UnionTypeDefinition([
'name' => $this->nameNode('Hello', $loc(6, 11)),
'types' => [$this->typeNode('World', $loc(14, 19))],
'loc' => $loc(0, 19)
])
],
'loc' => $loc(0, 19)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Union with two types
*/
public function testUnionWithTwoTypes()
{
$body = 'union Hello = Wo | Rld';
$doc = Parser::parse($body);
$loc = $this->createLocFn($body);
$expected = new Document([
'definitions' => [
new UnionTypeDefinition([
'name' => $this->nameNode('Hello', $loc(6, 11)),
'types' => [
$this->typeNode('Wo', $loc(14, 16)),
$this->typeNode('Rld', $loc(19, 22))
],
'loc' => $loc(0, 22)
])
],
'loc' => $loc(0, 22)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Scalar
*/
public function testScalar()
{
$body = 'scalar Hello';
$doc = Parser::parse($body);
$loc = $this->createLocFn($body);
$expected = new Document([
'definitions' => [
new ScalarTypeDefinition([
'name' => $this->nameNode('Hello', $loc(7, 12)),
'loc' => $loc(0, 12)
])
],
'loc' => $loc(0, 12)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Simple input object
*/
public function testSimpleInputObject()
{
$body = '
input Hello {
world: String
}';
$doc = Parser::parse($body);
$loc = $this->createLocFn($body);
$expected = new Document([
'definitions' => [
new InputObjectTypeDefinition([
'name' => $this->nameNode('Hello', $loc(7, 12)),
'fields' => [
$this->inputValueNode(
$this->nameNode('world', $loc(17, 22)),
$this->typeNode('String', $loc(24, 30)),
null,
$loc(17, 30)
)
],
'loc' => $loc(1, 32)
])
],
'loc' => $loc(1, 32)
]);
$this->assertEquals($expected, $doc);
}
/**
* @it Simple input object with args should fail
*/
public function testSimpleInputObjectWithArgsShouldFail()
{
$body = '
input Hello {
world(foo: Int): String
}';
$this->setExpectedException('GraphQL\SyntaxError');
Parser::parse($body);
}
private function createLocFn($body)
{
return function($start, $end) use ($body) {
return new Location($start, $end, new Source($body));
};
}
private function typeNode($name, $loc)
{
return new NamedType([
'name' => new Name(['value' => $name, 'loc' => $loc]),
'loc' => $loc
]);
}
private function nameNode($name, $loc)
{
return new Name([
'value' => $name,
'loc' => $loc
]);
}
private function fieldNode($name, $type, $loc)
{
return $this->fieldNodeWithArgs($name, $type, [], $loc);
}
private function fieldNodeWithArgs($name, $type, $args, $loc)
{
return new FieldDefinition([
'name' => $name,
'arguments' => $args,
'type' => $type,
'loc' => $loc
]);
}
private function enumValueNode($name, $loc)
{
return new EnumValueDefinition([
'name' => $this->nameNode($name, $loc),
'loc' => $loc
]);
}
private function inputValueNode($name, $type, $defaultValue, $loc)
{
return new InputValueDefinition([
'name' => $name,
'type' => $type,
'defaultValue' => $defaultValue,
'loc' => $loc
]);
}
}

View File

@ -17,6 +17,12 @@ query queryName($foo: ComplexType, $site: Site = MOBILE) {
}
}
}
... @skip(unless: $foo) {
id
}
... {
id
}
}
}
@ -28,6 +34,19 @@ 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"})
}