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 // language/parser.js
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;
@ -21,10 +28,16 @@ use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\NamedType;
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\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\TypeSystemDefinition;
use GraphQL\Language\AST\UnionTypeDefinition;
use GraphQL\Language\AST\Variable; use GraphQL\Language\AST\Variable;
use GraphQL\Language\AST\VariableDefinition; use GraphQL\Language\AST\VariableDefinition;
use GraphQL\SyntaxError; use GraphQL\SyntaxError;
@ -48,7 +61,7 @@ class Parser
* @param array $options * @param array $options
* @return Document * @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); $sourceObj = $source instanceof Source ? $source : new Source($source);
$parser = new self($sourceObj, $options); $parser = new self($sourceObj, $options);
@ -80,7 +93,7 @@ class Parser
*/ */
private $token; private $token;
function __construct(Source $source, array $options = array()) function __construct(Source $source, array $options = [])
{ {
$this->lexer = new Lexer($source); $this->lexer = new Lexer($source);
$this->source = $source; $this->source = $source;
@ -212,13 +225,13 @@ class Parser
* @param callable $parseFn * @param callable $parseFn
* @param int $closeKind * @param int $closeKind
* @return array * @return array
* @throws Exception * @throws SyntaxError
*/ */
function any($openKind, $parseFn, $closeKind) function any($openKind, $parseFn, $closeKind)
{ {
$this->expect($openKind); $this->expect($openKind);
$nodes = array(); $nodes = [];
while (!$this->skip($closeKind)) { while (!$this->skip($closeKind)) {
$nodes[] = $parseFn($this); $nodes[] = $parseFn($this);
} }
@ -235,13 +248,13 @@ class Parser
* @param $parseFn * @param $parseFn
* @param $closeKind * @param $closeKind
* @return array * @return array
* @throws Exception * @throws SyntaxError
*/ */
function many($openKind, $parseFn, $closeKind) function many($openKind, $parseFn, $closeKind)
{ {
$this->expect($openKind); $this->expect($openKind);
$nodes = array($parseFn($this)); $nodes = [$parseFn($this)];
while (!$this->skip($closeKind)) { while (!$this->skip($closeKind)) {
$nodes[] = $parseFn($this); $nodes[] = $parseFn($this);
} }
@ -252,16 +265,16 @@ class Parser
* Converts a name lex token into a name parse node. * Converts a name lex token into a name parse node.
* *
* @return Name * @return Name
* @throws Exception * @throws SyntaxError
*/ */
function parseName() function parseName()
{ {
$token = $this->expect(Token::NAME); $token = $this->expect(Token::NAME);
return new Name(array( return new Name([
'value' => $token->value, 'value' => $token->value,
'loc' => $this->loc($token->start) 'loc' => $this->loc($token->start)
)); ]);
} }
/** /**
@ -280,66 +293,113 @@ class Parser
* Implements the parsing rules in the Document section. * Implements the parsing rules in the Document section.
* *
* @return Document * @return Document
* @throws Exception * @throws SyntaxError
*/ */
function parseDocument() function parseDocument()
{ {
$start = $this->token->start; $start = $this->token->start;
$definitions = array(); $definitions = [];
do { do {
if ($this->peek(Token::BRACE_L)) { $definitions[] = $this->parseDefinition();
$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();
}
} while (!$this->skip(Token::EOF)); } while (!$this->skip(Token::EOF));
return new Document(array( return new Document([
'definitions' => $definitions, 'definitions' => $definitions,
'loc' => $this->loc($start) '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. // Implements the parsing rules in the Operations section.
/** /**
* @return OperationDefinition * @return OperationDefinition
* @throws Exception * @throws SyntaxError
*/ */
function parseOperationDefinition() function parseOperationDefinition()
{ {
$start = $this->token->start; $start = $this->token->start;
if ($this->peek(Token::BRACE_L)) { if ($this->peek(Token::BRACE_L)) {
return new OperationDefinition(array( return new OperationDefinition([
'operation' => 'query', 'operation' => 'query',
'name' => null, 'name' => null,
'variableDefinitions' => null, 'variableDefinitions' => null,
'directives' => array(), 'directives' => [],
'selectionSet' => $this->parseSelectionSet(), 'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} }
$operationToken = $this->expect(Token::NAME); $operation = $this->parseOperationType();
$operation = $operationToken->value;
return new OperationDefinition(array( $name = null;
if ($this->peek(Token::NAME)) {
$name = $this->parseName();
}
return new OperationDefinition([
'operation' => $operation, 'operation' => $operation,
'name' => $this->parseName(), 'name' => $name,
'variableDefinitions' => $this->parseVariableDefinitions(), 'variableDefinitions' => $this->parseVariableDefinitions(),
'directives' => $this->parseDirectives(), 'directives' => $this->parseDirectives(),
'selectionSet' => $this->parseSelectionSet(), 'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start) '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) ? return $this->peek(Token::PAREN_L) ?
$this->many( $this->many(
Token::PAREN_L, Token::PAREN_L,
array($this, 'parseVariableDefinition'), [$this, 'parseVariableDefinition'],
Token::PAREN_R Token::PAREN_R
) : ) :
array(); [];
} }
/** /**
* @return VariableDefinition * @return VariableDefinition
* @throws Exception * @throws SyntaxError
*/ */
function parseVariableDefinition() function parseVariableDefinition()
{ {
@ -368,44 +428,47 @@ class Parser
$this->expect(Token::COLON); $this->expect(Token::COLON);
$type = $this->parseType(); $type = $this->parseType();
return new VariableDefinition(array( return new VariableDefinition([
'variable' => $var, 'variable' => $var,
'type' => $type, 'type' => $type,
'defaultValue' => 'defaultValue' =>
($this->skip(Token::EQUALS) ? $this->parseValueLiteral(true) : null), ($this->skip(Token::EQUALS) ? $this->parseValueLiteral(true) : null),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} }
/** /**
* @return Variable * @return Variable
* @throws Exception * @throws SyntaxError
*/ */
function parseVariable() { function parseVariable()
{
$start = $this->token->start; $start = $this->token->start;
$this->expect(Token::DOLLAR); $this->expect(Token::DOLLAR);
return new Variable(array( return new Variable([
'name' => $this->parseName(), 'name' => $this->parseName(),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} }
/** /**
* @return SelectionSet * @return SelectionSet
*/ */
function parseSelectionSet() { function parseSelectionSet()
{
$start = $this->token->start; $start = $this->token->start;
return new SelectionSet(array( return new SelectionSet([
'selections' => $this->many(Token::BRACE_L, array($this, 'parseSelection'), Token::BRACE_R), 'selections' => $this->many(Token::BRACE_L, [$this, 'parseSelection'], Token::BRACE_R),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} }
/** /**
* @return mixed * @return mixed
*/ */
function parseSelection() { function parseSelection()
{
return $this->peek(Token::SPREAD) ? return $this->peek(Token::SPREAD) ?
$this->parseFragment() : $this->parseFragment() :
$this->parseField(); $this->parseField();
@ -414,7 +477,8 @@ class Parser
/** /**
* @return Field * @return Field
*/ */
function parseField() { function parseField()
{
$start = $this->token->start; $start = $this->token->start;
$nameOrAlias = $this->parseName(); $nameOrAlias = $this->parseName();
@ -426,28 +490,29 @@ class Parser
$name = $nameOrAlias; $name = $nameOrAlias;
} }
return new Field(array( return new Field([
'alias' => $alias, 'alias' => $alias,
'name' => $name, 'name' => $name,
'arguments' => $this->parseArguments(), 'arguments' => $this->parseArguments(),
'directives' => $this->parseDirectives(), 'directives' => $this->parseDirectives(),
'selectionSet' => $this->peek(Token::BRACE_L) ? $this->parseSelectionSet() : null, 'selectionSet' => $this->peek(Token::BRACE_L) ? $this->parseSelectionSet() : null,
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} }
/** /**
* @return array<Argument> * @return array<Argument>
*/ */
function parseArguments() { function parseArguments()
{
return $this->peek(Token::PAREN_L) ? return $this->peek(Token::PAREN_L) ?
$this->many(Token::PAREN_L, array($this, 'parseArgument'), Token::PAREN_R) : $this->many(Token::PAREN_L, [$this, 'parseArgument'], Token::PAREN_R) :
array(); [];
} }
/** /**
* @return Argument * @return Argument
* @throws Exception * @throws SyntaxError
*/ */
function parseArgument() function parseArgument()
{ {
@ -457,44 +522,52 @@ class Parser
$this->expect(Token::COLON); $this->expect(Token::COLON);
$value = $this->parseValueLiteral(false); $value = $this->parseValueLiteral(false);
return new Argument(array( return new Argument([
'name' => $name, 'name' => $name,
'value' => $value, 'value' => $value,
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} }
// Implements the parsing rules in the Fragments section. // Implements the parsing rules in the Fragments section.
/** /**
* @return FragmentSpread|InlineFragment * @return FragmentSpread|InlineFragment
* @throws Exception * @throws SyntaxError
*/ */
function parseFragment() { function parseFragment()
{
$start = $this->token->start; $start = $this->token->start;
$this->expect(Token::SPREAD); $this->expect(Token::SPREAD);
if ($this->token->value === 'on') { if ($this->peek(Token::NAME) && $this->token->value !== 'on') {
$this->advance(); return new FragmentSpread([
return new InlineFragment(array(
'typeCondition' => $this->parseNamedType(),
'directives' => $this->parseDirectives(),
'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start)
));
}
return new FragmentSpread(array(
'name' => $this->parseFragmentName(), 'name' => $this->parseFragmentName(),
'directives' => $this->parseDirectives(), 'directives' => $this->parseDirectives(),
'loc' => $this->loc($start) '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 * @return FragmentDefinition
* @throws SyntaxError * @throws SyntaxError
*/ */
function parseFragmentDefinition() { function parseFragmentDefinition()
{
$start = $this->token->start; $start = $this->token->start;
$this->expectKeyword('fragment'); $this->expectKeyword('fragment');
@ -502,13 +575,13 @@ class Parser
$this->expectKeyword('on'); $this->expectKeyword('on');
$typeCondition = $this->parseNamedType(); $typeCondition = $this->parseNamedType();
return new FragmentDefinition(array( return new FragmentDefinition([
'name' => $name, 'name' => $name,
'typeCondition' => $typeCondition, 'typeCondition' => $typeCondition,
'directives' => $this->parseDirectives(), 'directives' => $this->parseDirectives(),
'selectionSet' => $this->parseSelectionSet(), 'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} }
// Implements the parsing rules in the Values section. // Implements the parsing rules in the Values section.
@ -519,7 +592,7 @@ class Parser
/** /**
* @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable * @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable
* @throws Exception * @throws SyntaxError
*/ */
function parseConstValue() function parseConstValue()
{ {
@ -531,7 +604,8 @@ class Parser
* @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable * @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable
* @throws SyntaxError * @throws SyntaxError
*/ */
function parseValueLiteral($isConst) { function parseValueLiteral($isConst)
{
$token = $this->token; $token = $this->token;
switch ($token->kind) { switch ($token->kind) {
case Token::BRACKET_L: case Token::BRACKET_L:
@ -540,35 +614,35 @@ class Parser
return $this->parseObject($isConst); return $this->parseObject($isConst);
case Token::INT: case Token::INT:
$this->advance(); $this->advance();
return new IntValue(array( return new IntValue([
'value' => $token->value, 'value' => $token->value,
'loc' => $this->loc($token->start) 'loc' => $this->loc($token->start)
)); ]);
case Token::FLOAT: case Token::FLOAT:
$this->advance(); $this->advance();
return new FloatValue(array( return new FloatValue([
'value' => $token->value, 'value' => $token->value,
'loc' => $this->loc($token->start) 'loc' => $this->loc($token->start)
)); ]);
case Token::STRING: case Token::STRING:
$this->advance(); $this->advance();
return new StringValue(array( return new StringValue([
'value' => $token->value, 'value' => $token->value,
'loc' => $this->loc($token->start) 'loc' => $this->loc($token->start)
)); ]);
case Token::NAME: case Token::NAME:
if ($token->value === 'true' || $token->value === 'false') { if ($token->value === 'true' || $token->value === 'false') {
$this->advance(); $this->advance();
return new BooleanValue(array( return new BooleanValue([
'value' => $token->value === 'true', 'value' => $token->value === 'true',
'loc' => $this->loc($token->start) 'loc' => $this->loc($token->start)
)); ]);
} else if ($token->value !== 'null') { } else if ($token->value !== 'null') {
$this->advance(); $this->advance();
return new EnumValue(array( return new EnumValue([
'value' => $token->value, 'value' => $token->value,
'loc' => $this->loc($token->start) 'loc' => $this->loc($token->start)
)); ]);
} }
break; break;
@ -589,25 +663,25 @@ class Parser
{ {
$start = $this->token->start; $start = $this->token->start;
$item = $isConst ? 'parseConstValue' : 'parseVariableValue'; $item = $isConst ? 'parseConstValue' : 'parseVariableValue';
return new ListValue(array( return new ListValue([
'values' => $this->any(Token::BRACKET_L, array($this, $item), Token::BRACKET_R), 'values' => $this->any(Token::BRACKET_L, [$this, $item], Token::BRACKET_R),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} }
function parseObject($isConst) function parseObject($isConst)
{ {
$start = $this->token->start; $start = $this->token->start;
$this->expect(Token::BRACE_L); $this->expect(Token::BRACE_L);
$fieldNames = array(); $fieldNames = [];
$fields = array(); $fields = [];
while (!$this->skip(Token::BRACE_R)) { while (!$this->skip(Token::BRACE_R)) {
$fields[] = $this->parseObjectField($isConst, $fieldNames); $fields[] = $this->parseObjectField($isConst, $fieldNames);
} }
return new ObjectValue(array( return new ObjectValue([
'fields' => $fields, 'fields' => $fields,
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} }
function parseObjectField($isConst, &$fieldNames) function parseObjectField($isConst, &$fieldNames)
@ -621,11 +695,11 @@ class Parser
$fieldNames[$name->value] = true; $fieldNames[$name->value] = true;
$this->expect(Token::COLON); $this->expect(Token::COLON);
return new ObjectField(array( return new ObjectField([
'name' => $name, 'name' => $name,
'value' => $this->parseValueLiteral($isConst), 'value' => $this->parseValueLiteral($isConst),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} }
// Implements the parsing rules in the Directives section. // Implements the parsing rules in the Directives section.
@ -635,7 +709,7 @@ class Parser
*/ */
function parseDirectives() function parseDirectives()
{ {
$directives = array(); $directives = [];
while ($this->peek(Token::AT)) { while ($this->peek(Token::AT)) {
$directives[] = $this->parseDirective(); $directives[] = $this->parseDirective();
} }
@ -644,17 +718,17 @@ class Parser
/** /**
* @return Directive * @return Directive
* @throws Exception * @throws SyntaxError
*/ */
function parseDirective() function parseDirective()
{ {
$start = $this->token->start; $start = $this->token->start;
$this->expect(Token::AT); $this->expect(Token::AT);
return new Directive(array( return new Directive([
'name' => $this->parseName(), 'name' => $this->parseName(),
'arguments' => $this->parseArguments(), 'arguments' => $this->parseArguments(),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} }
// Implements the parsing rules in the Types section. // Implements the parsing rules in the Types section.
@ -672,18 +746,18 @@ class Parser
if ($this->skip(Token::BRACKET_L)) { if ($this->skip(Token::BRACKET_L)) {
$type = $this->parseType(); $type = $this->parseType();
$this->expect(Token::BRACKET_R); $this->expect(Token::BRACKET_R);
$type = new ListType(array( $type = new ListType([
'type' => $type, 'type' => $type,
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} else { } else {
$type = $this->parseNamedType(); $type = $this->parseNamedType();
} }
if ($this->skip(Token::BANG)) { if ($this->skip(Token::BANG)) {
return new NonNullType(array( return new NonNullType([
'type' => $type, 'type' => $type,
'loc' => $this->loc($start) 'loc' => $this->loc($start)
)); ]);
} }
return $type; return $type;
@ -697,6 +771,333 @@ class Parser
'name' => $this->parseName(), 'name' => $this->parseName(),
'loc' => $this->loc($start) '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 <?php
namespace GraphQL\Tests\Language; namespace GraphQL\Tests\Language;
use GraphQL\Error;
use GraphQL\Language\AST\Argument; use GraphQL\Language\AST\Argument;
use GraphQL\Language\AST\Document; use GraphQL\Language\AST\Document;
use GraphQL\Language\AST\Field; use GraphQL\Language\AST\Field;
@ -10,16 +9,20 @@ use GraphQL\Language\AST\Location;
use GraphQL\Language\AST\Name; use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\OperationDefinition; use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Language\AST\SelectionSet; use GraphQL\Language\AST\SelectionSet;
use GraphQL\Language\AST\StringValue;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Language\Source; use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
use GraphQL\SyntaxError; use GraphQL\SyntaxError;
use GraphQL\Utils;
class ParserTest extends \PHPUnit_Framework_TestCase class ParserTest extends \PHPUnit_Framework_TestCase
{ {
/**
* @it accepts option to not include source
*/
public function testAcceptsOptionToNotIncludeSource() public function testAcceptsOptionToNotIncludeSource()
{ {
// accepts option to not include source
$actual = Parser::parse('{ field }', ['noSource' => true]); $actual = Parser::parse('{ field }', ['noSource' => true]);
$expected = new Document([ $expected = new Document([
@ -54,6 +57,9 @@ class ParserTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, $actual); $this->assertEquals($expected, $actual);
} }
/**
* @it parse provides useful errors
*/
public function testParseProvidesUsefulErrors() public function testParseProvidesUsefulErrors()
{ {
$run = function($num, $str, $expectedMessage, $expectedPositions = null, $expectedLocations = null) { $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, $run(1,
'{ ...MissingOn } '{ ...MissingOn }
fragment MissingOn Type 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(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(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(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() public function testParseProvidesUsefulErrorWhenUsingSource()
{ {
try { try {
Parser::parse(new Source('query', 'MyQuery.graphql')); Parser::parse(new Source('query', 'MyQuery.graphql'));
$this->fail('Expected exception not thrown'); $this->fail('Expected exception not thrown');
} catch (SyntaxError $e) { } 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() public function testParsesVariableInlineValues()
{ {
// Following line should not throw: // Following line should not throw:
Parser::parse('{ field(complex: { a: { b: [ $var ] } }) }'); Parser::parse('{ field(complex: { a: { b: [ $var ] } }) }');
} }
/**
* @it parses constant default values
*/
public function testParsesConstantDefaultValues() public function testParsesConstantDefaultValues()
{ {
try { try {
@ -114,39 +129,71 @@ fragment MissingOn Type
} }
} }
public function testDuplicateKeysInInputObjectIsSyntaxError() /**
{ * @it does not accept fragments spread of "on"
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()
);
}
}
public function testDoesNotAcceptFragmentsNamedOn() public function testDoesNotAcceptFragmentsNamedOn()
{ {
// does not accept fragments named "on"
$this->setExpectedException('GraphQL\SyntaxError', 'Syntax Error GraphQL (1:10) Unexpected Name "on"'); $this->setExpectedException('GraphQL\SyntaxError', 'Syntax Error GraphQL (1:10) Unexpected Name "on"');
Parser::parse('fragment on on on { on }'); Parser::parse('fragment on on on { on }');
} }
/**
* @it does not accept fragments spread of "on"
*/
public function testDoesNotAcceptFragmentSpreadOfOn() public function testDoesNotAcceptFragmentSpreadOfOn()
{ {
// does not accept fragments spread of "on"
$this->setExpectedException('GraphQL\SyntaxError', 'Syntax Error GraphQL (1:9) Expected Name, found }'); $this->setExpectedException('GraphQL\SyntaxError', 'Syntax Error GraphQL (1:9) Expected Name, found }');
Parser::parse('{ ...on }'); Parser::parse('{ ...on }');
} }
/**
* @it does not allow null as value
*/
public function testDoesNotAllowNullAsValue() public function testDoesNotAllowNullAsValue()
{ {
$this->setExpectedException('GraphQL\SyntaxError', 'Syntax Error GraphQL (1:39) Unexpected Name "null"'); $this->setExpectedException('GraphQL\SyntaxError', 'Syntax Error GraphQL (1:39) Unexpected Name "null"');
Parser::parse('{ fieldWithNullableStringInput(input: 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() public function testParsesKitchenSink()
{ {
// Following should not throw: // Following should not throw:
@ -155,14 +202,17 @@ fragment MissingOn Type
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
} }
/**
* allows non-keywords anywhere a Name is allowed
*/
public function testAllowsNonKeywordsAnywhereANameIsAllowed() public function testAllowsNonKeywordsAnywhereANameIsAllowed()
{ {
// allows non-keywords anywhere a Name is allowed
$nonKeywords = [ $nonKeywords = [
'on', 'on',
'fragment', 'fragment',
'query', 'query',
'mutation', 'mutation',
'subscription',
'true', 'true',
'false' '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() public function testParseCreatesAst()
{ {
$source = new Source('{ $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 { fragment frag on Friend {
foo(size: $size, bar: $b, obj: {key: "value"}) foo(size: $size, bar: $b, obj: {key: "value"})
} }