diff --git a/src/Language/AST/EnumTypeDefinition.php b/src/Language/AST/EnumTypeDefinition.php index 6e91b85..fc1fc39 100644 --- a/src/Language/AST/EnumTypeDefinition.php +++ b/src/Language/AST/EnumTypeDefinition.php @@ -13,6 +13,11 @@ class EnumTypeDefinition extends Node implements TypeDefinition */ public $name; + /** + * @var Directive[] + */ + public $directives; + /** * @var EnumValueDefinition[] */ diff --git a/src/Language/AST/EnumValueDefinition.php b/src/Language/AST/EnumValueDefinition.php index c2b0a49..d1a751d 100644 --- a/src/Language/AST/EnumValueDefinition.php +++ b/src/Language/AST/EnumValueDefinition.php @@ -12,4 +12,9 @@ class EnumValueDefinition extends Node * @var Name */ public $name; + + /** + * @var Directive[] + */ + public $directives; } diff --git a/src/Language/AST/FieldDefinition.php b/src/Language/AST/FieldDefinition.php index 801e8d0..13d24b6 100644 --- a/src/Language/AST/FieldDefinition.php +++ b/src/Language/AST/FieldDefinition.php @@ -22,4 +22,9 @@ class FieldDefinition extends Node * @var Type */ public $type; + + /** + * @var Directive[] + */ + public $directives; } diff --git a/src/Language/AST/InputObjectTypeDefinition.php b/src/Language/AST/InputObjectTypeDefinition.php index af4d67c..4c887d1 100644 --- a/src/Language/AST/InputObjectTypeDefinition.php +++ b/src/Language/AST/InputObjectTypeDefinition.php @@ -13,6 +13,11 @@ class InputObjectTypeDefinition extends Node implements TypeDefinition */ public $name; + /** + * @var Directive[] + */ + public $directives; + /** * @var InputValueDefinition[] */ diff --git a/src/Language/AST/InputValueDefinition.php b/src/Language/AST/InputValueDefinition.php index bbf7b72..06af8ff 100644 --- a/src/Language/AST/InputValueDefinition.php +++ b/src/Language/AST/InputValueDefinition.php @@ -22,4 +22,9 @@ class InputValueDefinition extends Node * @var Value */ public $defaultValue; + + /** + * @var Directive[] + */ + public $directives; } \ No newline at end of file diff --git a/src/Language/AST/InterfaceTypeDefinition.php b/src/Language/AST/InterfaceTypeDefinition.php index da0f402..bdf893e 100644 --- a/src/Language/AST/InterfaceTypeDefinition.php +++ b/src/Language/AST/InterfaceTypeDefinition.php @@ -13,6 +13,11 @@ class InterfaceTypeDefinition extends Node implements TypeDefinition */ public $name; + /** + * @var Directive[] + */ + public $directives; + /** * @var FieldDefinition[] */ diff --git a/src/Language/AST/Node.php b/src/Language/AST/Node.php index b862b91..328dfdc 100644 --- a/src/Language/AST/Node.php +++ b/src/Language/AST/Node.php @@ -3,6 +3,10 @@ namespace GraphQL\Language\AST; use GraphQL\Utils; +/** + * Class Node + * @package GraphQL\Language\AST + */ abstract class Node { // constants from language/kinds.js: diff --git a/src/Language/AST/ObjectTypeDefinition.php b/src/Language/AST/ObjectTypeDefinition.php index fb4e19b..e28242a 100644 --- a/src/Language/AST/ObjectTypeDefinition.php +++ b/src/Language/AST/ObjectTypeDefinition.php @@ -18,6 +18,11 @@ class ObjectTypeDefinition extends Node implements TypeDefinition */ public $interfaces = []; + /** + * @var Directive[] + */ + public $directives; + /** * @var FieldDefinition[] */ diff --git a/src/Language/AST/ScalarTypeDefinition.php b/src/Language/AST/ScalarTypeDefinition.php index 1a814f2..82028ee 100644 --- a/src/Language/AST/ScalarTypeDefinition.php +++ b/src/Language/AST/ScalarTypeDefinition.php @@ -4,10 +4,18 @@ namespace GraphQL\Language\AST; class ScalarTypeDefinition extends Node implements TypeDefinition { + /** + * @var string + */ public $kind = Node::SCALAR_TYPE_DEFINITION; /** * @var Name */ public $name; + + /** + * @var Directive[] + */ + public $directives; } diff --git a/src/Language/AST/SchemaDefinition.php b/src/Language/AST/SchemaDefinition.php index 9f4440a..194b14e 100644 --- a/src/Language/AST/SchemaDefinition.php +++ b/src/Language/AST/SchemaDefinition.php @@ -8,6 +8,11 @@ class SchemaDefinition extends Node implements TypeSystemDefinition */ public $kind = Node::SCHEMA_DEFINITION; + /** + * @var Directive[] + */ + public $directives; + /** * @var OperationTypeDefinition[] */ diff --git a/src/Language/AST/UnionTypeDefinition.php b/src/Language/AST/UnionTypeDefinition.php index 589c7ab..be57abf 100644 --- a/src/Language/AST/UnionTypeDefinition.php +++ b/src/Language/AST/UnionTypeDefinition.php @@ -13,6 +13,11 @@ class UnionTypeDefinition extends Node implements TypeDefinition */ public $name; + /** + * @var Directive[] + */ + public $directives; + /** * @var NamedType[] */ diff --git a/src/Language/Parser.php b/src/Language/Parser.php index 7e79dab..6a83a4a 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -53,11 +53,6 @@ class Parser * in the source that they correspond to. This configuration flag * disables that behavior for performance or testing.) * - * noSource: boolean, - * By default, the parser creates AST nodes that contain a reference - * to the source that they were created from. This configuration flag - * disables that behavior for performance or testing. - * * @param Source|string $source * @param array $options * @return Document @@ -69,66 +64,77 @@ class Parser return $parser->parseDocument(); } - /** - * @var Source - */ - private $source; /** - * @var array + * Given a string containing a GraphQL value (ex. `[42]`), parse the AST for + * that value. + * Throws GraphQLError if a syntax error is encountered. + * + * This is useful within tools that operate upon GraphQL Values directly and + * in isolation of complete GraphQL documents. + * + * Consider providing the results to the utility function: valueFromAST(). + * + * @param Source|string $source + * @param array $options + * @return BooleanValue|EnumValue|FloatValue|IntValue|ListValue|ObjectValue|StringValue|Variable */ - private $options; + public static function parseValue($source, array $options = []) + { + $sourceObj = $source instanceof Source ? $source : new Source($source); + $parser = new Parser($sourceObj, $options); + $parser->expect(Token::SOF); + $value = $parser->parseValueLiteral(false); + $parser->expect(Token::EOF); + return $value; + } /** - * @var int + * Given a string containing a GraphQL Type (ex. `[Int!]`), parse the AST for + * that type. + * Throws GraphQLError if a syntax error is encountered. + * + * This is useful within tools that operate upon GraphQL Types directly and + * in isolation of complete GraphQL documents. + * + * Consider providing the results to the utility function: typeFromAST(). + * @param Source|string $source + * @param array $options + * @return ListType|Name|NonNullType */ - private $prevEnd; + public static function parseType($source, array $options = []) + { + $sourceObj = $source instanceof Source ? $source : new Source($source); + $parser = new Parser($sourceObj, $options); + $parser->expect(Token::SOF); + $type = $parser->parseTypeReference(); + $parser->expect(Token::EOF); + return $type; + } /** * @var Lexer */ private $lexer; - /** - * @var Token - */ - private $token; - function __construct(Source $source, array $options = []) { - $this->lexer = new Lexer($source); - $this->source = $source; - $this->options = $options; - $this->prevEnd = 0; - $this->token = $this->lexer->nextToken(); + $this->lexer = new Lexer($source, $options); } /** * Returns a location object, used to identify the place in * the source that created a given parsed object. * - * @param int $start + * @param Token $startToken * @return Location|null */ - function loc($start) + function loc(Token $startToken) { - if (!empty($this->options['noLocation'])) { - return null; + if (empty($this->lexer->options['noLocation'])) { + return new Location($startToken, $this->lexer->lastToken, $this->lexer->source); } - if (!empty($this->options['noSource'])) { - return new Location($start, $this->prevEnd); - } - return new Location($start, $this->prevEnd, $this->source); - } - - /** - * Moves the internal parser object to the next lexed token. - */ - function advance() - { - $prevEnd = $this->token->end; - $this->prevEnd = $prevEnd; - $this->token = $this->lexer->nextToken($prevEnd); + return null; } /** @@ -139,7 +145,7 @@ class Parser */ function peek($kind) { - return $this->token->kind === $kind; + return $this->lexer->token->kind === $kind; } /** @@ -151,10 +157,10 @@ class Parser */ function skip($kind) { - $match = $this->token->kind === $kind; + $match = $this->lexer->token->kind === $kind; if ($match) { - $this->advance(); + $this->lexer->advance(); } return $match; } @@ -168,17 +174,17 @@ class Parser */ function expect($kind) { - $token = $this->token; + $token = $this->lexer->token; if ($token->kind === $kind) { - $this->advance(); + $this->lexer->advance(); return $token; } throw new SyntaxError( - $this->source, + $this->lexer->source, $token->start, - "Expected " . Token::getKindDescription($kind) . ", found " . $token->getDescription() + "Expected $kind, found " . $token->getDescription() ); } @@ -193,14 +199,14 @@ class Parser */ function expectKeyword($value) { - $token = $this->token; + $token = $this->lexer->token; if ($token->kind === Token::NAME && $token->value === $value) { - $this->advance(); + $this->lexer->advance(); return $token; } throw new SyntaxError( - $this->source, + $this->lexer->source, $token->start, 'Expected "' . $value . '", found ' . $token->getDescription() ); @@ -212,8 +218,8 @@ class Parser */ function unexpected(Token $atToken = null) { - $token = $atToken ?: $this->token; - return new SyntaxError($this->source, $token->start, "Unexpected " . $token->getDescription()); + $token = $atToken ?: $this->lexer->token; + return new SyntaxError($this->lexer->source, $token->start, "Unexpected " . $token->getDescription()); } /** @@ -274,22 +280,10 @@ class Parser return new Name([ 'value' => $token->value, - 'loc' => $this->loc($token->start) + 'loc' => $this->loc($token) ]); } - /** - * @return Name - * @throws SyntaxError - */ - function parseFragmentName() - { - if ($this->token->value === 'on') { - throw $this->unexpected(); - } - return $this->parseName(); - } - /** * Implements the parsing rules in the Document section. * @@ -298,9 +292,10 @@ class Parser */ function parseDocument() { - $start = $this->token->start; - $definitions = []; + $start = $this->lexer->token; + $this->expect(Token::SOF); + $definitions = []; do { $definitions[] = $this->parseDefinition(); } while (!$this->skip(Token::EOF)); @@ -322,10 +317,9 @@ class Parser } if ($this->peek(Token::NAME)) { - switch ($this->token->value) { + switch ($this->lexer->token->value) { case 'query': case 'mutation': - // Note: subscription is an experimental non-spec addition. case 'subscription': return $this->parseOperationDefinition(); @@ -357,7 +351,7 @@ class Parser */ function parseOperationDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; if ($this->peek(Token::BRACE_L)) { return new OperationDefinition([ 'operation' => 'query', @@ -423,11 +417,11 @@ class Parser */ function parseVariableDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $var = $this->parseVariable(); $this->expect(Token::COLON); - $type = $this->parseType(); + $type = $this->parseTypeReference(); return new VariableDefinition([ 'variable' => $var, @@ -444,7 +438,7 @@ class Parser */ function parseVariable() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expect(Token::DOLLAR); return new Variable([ @@ -458,7 +452,7 @@ class Parser */ function parseSelectionSet() { - $start = $this->token->start; + $start = $this->lexer->token; return new SelectionSet([ 'selections' => $this->many(Token::BRACE_L, [$this, 'parseSelection'], Token::BRACE_R), 'loc' => $this->loc($start) @@ -466,6 +460,11 @@ class Parser } /** + * Selection : + * - Field + * - FragmentSpread + * - InlineFragment + * * @return mixed */ function parseSelection() @@ -480,7 +479,7 @@ class Parser */ function parseField() { - $start = $this->token->start; + $start = $this->lexer->token; $nameOrAlias = $this->parseName(); if ($this->skip(Token::COLON)) { @@ -517,7 +516,7 @@ class Parser */ function parseArgument() { - $start = $this->token->start; + $start = $this->lexer->token; $name = $this->parseName(); $this->expect(Token::COLON); @@ -538,10 +537,10 @@ class Parser */ function parseFragment() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expect(Token::SPREAD); - if ($this->peek(Token::NAME) && $this->token->value !== 'on') { + if ($this->peek(Token::NAME) && $this->lexer->token->value !== 'on') { return new FragmentSpread([ 'name' => $this->parseFragmentName(), 'directives' => $this->parseDirectives(), @@ -550,8 +549,8 @@ class Parser } $typeCondition = null; - if ($this->token->value === 'on') { - $this->advance(); + if ($this->lexer->token->value === 'on') { + $this->lexer->advance(); $typeCondition = $this->parseNamedType(); } @@ -569,7 +568,7 @@ class Parser */ function parseFragmentDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expectKeyword('fragment'); $name = $this->parseFragmentName(); @@ -585,64 +584,77 @@ class Parser ]); } - // Implements the parsing rules in the Values section. - function parseVariableValue() - { - return $this->parseValueLiteral(false); - } - /** - * @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable + * @return Name * @throws SyntaxError */ - function parseConstValue() + function parseFragmentName() { - return $this->parseValueLiteral(true); + if ($this->lexer->token->value === 'on') { + throw $this->unexpected(); + } + return $this->parseName(); } + // Implements the parsing rules in the Values section. + /** + * Value[Const] : + * - [~Const] Variable + * - IntValue + * - FloatValue + * - StringValue + * - BooleanValue + * - EnumValue + * - ListValue[?Const] + * - ObjectValue[?Const] + * + * BooleanValue : one of `true` `false` + * + * EnumValue : Name but not `true`, `false` or `null` + * * @param $isConst - * @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable + * @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable|ListValue|ObjectValue * @throws SyntaxError */ function parseValueLiteral($isConst) { - $token = $this->token; + $token = $this->lexer->token; switch ($token->kind) { case Token::BRACKET_L: return $this->parseArray($isConst); case Token::BRACE_L: return $this->parseObject($isConst); case Token::INT: - $this->advance(); + $this->lexer->advance(); return new IntValue([ 'value' => $token->value, - 'loc' => $this->loc($token->start) + 'loc' => $this->loc($token) ]); case Token::FLOAT: - $this->advance(); + $this->lexer->advance(); return new FloatValue([ 'value' => $token->value, - 'loc' => $this->loc($token->start) + 'loc' => $this->loc($token) ]); case Token::STRING: - $this->advance(); + $this->lexer->advance(); return new StringValue([ 'value' => $token->value, - 'loc' => $this->loc($token->start) + 'loc' => $this->loc($token) ]); case Token::NAME: if ($token->value === 'true' || $token->value === 'false') { - $this->advance(); + $this->lexer->advance(); return new BooleanValue([ 'value' => $token->value === 'true', - 'loc' => $this->loc($token->start) + 'loc' => $this->loc($token) ]); } else if ($token->value !== 'null') { - $this->advance(); + $this->lexer->advance(); return new EnumValue([ 'value' => $token->value, - 'loc' => $this->loc($token->start) + 'loc' => $this->loc($token) ]); } break; @@ -656,13 +668,30 @@ class Parser throw $this->unexpected(); } + /** + * @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable + * @throws SyntaxError + */ + function parseConstValue() + { + return $this->parseValueLiteral(true); + } + + /** + * @return BooleanValue|EnumValue|FloatValue|IntValue|ListValue|ObjectValue|StringValue|Variable + */ + function parseVariableValue() + { + return $this->parseValueLiteral(false); + } + /** * @param bool $isConst * @return ListValue */ function parseArray($isConst) { - $start = $this->token->start; + $start = $this->lexer->token; $item = $isConst ? 'parseConstValue' : 'parseVariableValue'; return new ListValue([ 'values' => $this->any(Token::BRACKET_L, [$this, $item], Token::BRACKET_R), @@ -670,9 +699,13 @@ class Parser ]); } + /** + * @param $isConst + * @return ObjectValue + */ function parseObject($isConst) { - $start = $this->token->start; + $start = $this->lexer->token; $this->expect(Token::BRACE_L); $fields = []; while (!$this->skip(Token::BRACE_R)) { @@ -684,9 +717,13 @@ class Parser ]); } + /** + * @param $isConst + * @return ObjectField + */ function parseObjectField($isConst) { - $start = $this->token->start; + $start = $this->lexer->token; $name = $this->parseName(); $this->expect(Token::COLON); @@ -718,7 +755,7 @@ class Parser */ function parseDirective() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expect(Token::AT); return new Directive([ 'name' => $this->parseName(), @@ -735,12 +772,12 @@ class Parser * @return ListType|Name|NonNullType * @throws SyntaxError */ - function parseType() + function parseTypeReference() { - $start = $this->token->start; + $start = $this->lexer->token; if ($this->skip(Token::BRACKET_L)) { - $type = $this->parseType(); + $type = $this->parseTypeReference(); $this->expect(Token::BRACKET_R); $type = new ListType([ 'type' => $type, @@ -761,7 +798,7 @@ class Parser function parseNamedType() { - $start = $this->token->start; + $start = $this->lexer->token; return new NamedType([ 'name' => $this->parseName(), @@ -773,6 +810,7 @@ class Parser /** * TypeSystemDefinition : + * - SchemaDefinition * - TypeDefinition * - TypeExtensionDefinition * - DirectiveDefinition @@ -791,7 +829,7 @@ class Parser function parseTypeSystemDefinition() { if ($this->peek(Token::NAME)) { - switch ($this->token->value) { + switch ($this->lexer->token->value) { case 'schema': return $this->parseSchemaDefinition(); case 'scalar': return $this->parseScalarTypeDefinition(); case 'type': return $this->parseObjectTypeDefinition(); @@ -813,8 +851,9 @@ class Parser */ function parseSchemaDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expectKeyword('schema'); + $directives = $this->parseDirectives(); $operationTypes = $this->many( Token::BRACE_L, @@ -823,14 +862,18 @@ class Parser ); return new SchemaDefinition([ + 'directives' => $directives, 'operationTypes' => $operationTypes, 'loc' => $this->loc($start) ]); } + /** + * @return OperationTypeDefinition + */ function parseOperationTypeDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $operation = $this->parseOperationType(); $this->expect(Token::COLON); $type = $this->parseNamedType(); @@ -848,12 +891,14 @@ class Parser */ function parseScalarTypeDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expectKeyword('scalar'); $name = $this->parseName(); + $directives = $this->parseDirectives(); return new ScalarTypeDefinition([ 'name' => $name, + 'directives' => $directives, 'loc' => $this->loc($start) ]); } @@ -864,10 +909,12 @@ class Parser */ function parseObjectTypeDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expectKeyword('type'); $name = $this->parseName(); $interfaces = $this->parseImplementsInterfaces(); + $directives = $this->parseDirectives(); + $fields = $this->any( Token::BRACE_L, [$this, 'parseFieldDefinition'], @@ -877,6 +924,7 @@ class Parser return new ObjectTypeDefinition([ 'name' => $name, 'interfaces' => $interfaces, + 'directives' => $directives, 'fields' => $fields, 'loc' => $this->loc($start) ]); @@ -888,11 +936,11 @@ class Parser function parseImplementsInterfaces() { $types = []; - if ($this->token->value === 'implements') { - $this->advance(); + if ($this->lexer->token->value === 'implements') { + $this->lexer->advance(); do { $types[] = $this->parseNamedType(); - } while (!$this->peek(Token::BRACE_L)); + } while ($this->peek(Token::NAME)); } return $types; } @@ -903,16 +951,18 @@ class Parser */ function parseFieldDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $name = $this->parseName(); $args = $this->parseArgumentDefs(); $this->expect(Token::COLON); - $type = $this->parseType(); + $type = $this->parseTypeReference(); + $directives = $this->parseDirectives(); return new FieldDefinition([ 'name' => $name, 'arguments' => $args, 'type' => $type, + 'directives' => $directives, 'loc' => $this->loc($start) ]); } @@ -934,18 +984,20 @@ class Parser */ function parseInputValueDef() { - $start = $this->token->start; + $start = $this->lexer->token; $name = $this->parseName(); $this->expect(Token::COLON); - $type = $this->parseType(); + $type = $this->parseTypeReference(); $defaultValue = null; if ($this->skip(Token::EQUALS)) { $defaultValue = $this->parseConstValue(); } + $directives = $this->parseDirectives(); return new InputValueDefinition([ 'name' => $name, 'type' => $type, 'defaultValue' => $defaultValue, + 'directives' => $directives, 'loc' => $this->loc($start) ]); } @@ -956,9 +1008,10 @@ class Parser */ function parseInterfaceTypeDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expectKeyword('interface'); $name = $this->parseName(); + $directives = $this->parseDirectives(); $fields = $this->any( Token::BRACE_L, [$this, 'parseFieldDefinition'], @@ -967,6 +1020,7 @@ class Parser return new InterfaceTypeDefinition([ 'name' => $name, + 'directives' => $directives, 'fields' => $fields, 'loc' => $this->loc($start) ]); @@ -978,20 +1032,26 @@ class Parser */ function parseUnionTypeDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expectKeyword('union'); $name = $this->parseName(); + $directives = $this->parseDirectives(); $this->expect(Token::EQUALS); $types = $this->parseUnionMembers(); return new UnionTypeDefinition([ 'name' => $name, + 'directives' => $directives, 'types' => $types, 'loc' => $this->loc($start) ]); } /** + * UnionMembers : + * - NamedType + * - UnionMembers | NamedType + * * @return NamedType[] */ function parseUnionMembers() @@ -1009,9 +1069,10 @@ class Parser */ function parseEnumTypeDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expectKeyword('enum'); $name = $this->parseName(); + $directives = $this->parseDirectives(); $values = $this->many( Token::BRACE_L, [$this, 'parseEnumValueDefinition'], @@ -1020,6 +1081,7 @@ class Parser return new EnumTypeDefinition([ 'name' => $name, + 'directives' => $directives, 'values' => $values, 'loc' => $this->loc($start) ]); @@ -1030,11 +1092,13 @@ class Parser */ function parseEnumValueDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $name = $this->parseName(); + $directives = $this->parseDirectives(); return new EnumValueDefinition([ 'name' => $name, + 'directives' => $directives, 'loc' => $this->loc($start) ]); } @@ -1045,9 +1109,10 @@ class Parser */ function parseInputObjectTypeDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expectKeyword('input'); $name = $this->parseName(); + $directives = $this->parseDirectives(); $fields = $this->any( Token::BRACE_L, [$this, 'parseInputValueDef'], @@ -1056,6 +1121,7 @@ class Parser return new InputObjectTypeDefinition([ 'name' => $name, + 'directives' => $directives, 'fields' => $fields, 'loc' => $this->loc($start) ]); @@ -1067,7 +1133,7 @@ class Parser */ function parseTypeExtensionDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expectKeyword('extend'); $definition = $this->parseObjectTypeDefinition(); @@ -1078,12 +1144,15 @@ class Parser } /** + * DirectiveDefinition : + * - directive @ Name ArgumentsDefinition? on DirectiveLocations + * * @return DirectiveDefinition * @throws SyntaxError */ function parseDirectiveDefinition() { - $start = $this->token->start; + $start = $this->lexer->token; $this->expectKeyword('directive'); $this->expect(Token::AT); $name = $this->parseName(); diff --git a/src/Language/Printer.php b/src/Language/Printer.php index b18e94e..775d640 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -115,33 +115,77 @@ class Printer 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::SCHEMA_DEFINITION => function(SchemaDefinition $def) { + return self::join([ + 'schema', + self::join($def->directives, ' '), + 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::SCALAR_TYPE_DEFINITION => function(ScalarTypeDefinition $def) { + return self::join(['scalar', $def->name, self::join($def->directives, ' ')], ' '); + }, Node::OBJECT_TYPE_DEFINITION => function(ObjectTypeDefinition $def) { - return 'type ' . $def->name . ' ' . - self::wrap('implements ', self::join($def->interfaces, ', '), ' ') . - self::block($def->fields); + return self::join([ + 'type', + $def->name, + self::wrap('implements ', self::join($def->interfaces, ', ')), + self::join($def->directives, ' '), + self::block($def->fields) + ], ' '); }, Node::FIELD_DEFINITION => function(FieldDefinition $def) { - return $def->name . self::wrap('(', self::join($def->arguments, ', '), ')') . ': ' . $def->type; + return $def->name + . self::wrap('(', self::join($def->arguments, ', '), ')') + . ': ' . $def->type + . self::wrap(' ', self::join($def->directives, ' ')); }, Node::INPUT_VALUE_DEFINITION => function(InputValueDefinition $def) { - return $def->name . ': ' . $def->type . self::wrap(' = ', $def->defaultValue); + return self::join([ + $def->name . ': ' . $def->type, + self::wrap('= ', $def->defaultValue), + self::join($def->directives, ' ') + ], ' '); }, Node::INTERFACE_TYPE_DEFINITION => function(InterfaceTypeDefinition $def) { - return 'interface ' . $def->name . ' ' . self::block($def->fields); + return self::join([ + 'interface', + $def->name, + self::join($def->directives, ' '), + self::block($def->fields) + ], ' '); }, Node::UNION_TYPE_DEFINITION => function(UnionTypeDefinition $def) { - return 'union ' . $def->name . ' = ' . self::join($def->types, ' | '); + return self::join([ + 'union', + $def->name, + self::join($def->directives, ' '), + '= ' . self::join($def->types, ' | ') + ], ' '); }, Node::ENUM_TYPE_DEFINITION => function(EnumTypeDefinition $def) { - return 'enum ' . $def->name . ' ' . self::block($def->values); + return self::join([ + 'enum', + $def->name, + self::join($def->directives, ' '), + self::block($def->values) + ], ' '); + }, + Node::ENUM_VALUE_DEFINITION => function(EnumValueDefinition $def) { + return self::join([ + $def->name, + self::join($def->directives, ' ') + ], ' '); }, - 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); + return self::join([ + 'input', + $def->name, + self::join($def->directives, ' '), + self::block($def->fields) + ], ' '); }, Node::TYPE_EXTENSION_DEFINITION => function(TypeExtensionDefinition $def) {return "extend {$def->definition}";}, Node::DIRECTIVE_DEFINITION => function(DirectiveDefinition $def) { @@ -162,12 +206,12 @@ class Printer } /** - * Given maybeArray, print an empty string if it is null or empty, otherwise - * print each item on it's own line, wrapped in an indented "{ }" block. + * Given array, print each item on its own line, wrapped in an + * indented "{ }" block. */ - public static function block($maybeArray) + public static function block($array) { - return self::length($maybeArray) ? self::indent("{\n" . self::join($maybeArray, "\n")) . "\n}" : ''; + return $array && self::length($array) ? self::indent("{\n" . self::join($array, "\n")) . "\n}" : '{}'; } public static function indent($maybeString) diff --git a/src/Language/Visitor.php b/src/Language/Visitor.php index 0fecebc..4cf650f 100644 --- a/src/Language/Visitor.php +++ b/src/Language/Visitor.php @@ -64,19 +64,17 @@ class Visitor Node::LIST_TYPE => ['type'], Node::NON_NULL_TYPE => ['type'], - Node::SCHEMA_DEFINITION => ['operationTypes'], + Node::SCHEMA_DEFINITION => ['directives', '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::SCALAR_TYPE_DEFINITION => ['name', 'directives'], + Node::OBJECT_TYPE_DEFINITION => ['name', 'interfaces', 'directives', 'fields'], + Node::FIELD_DEFINITION => ['name', 'arguments', 'type', 'directives'], + Node::INPUT_VALUE_DEFINITION => ['name', 'type', 'defaultValue', 'directives'], + Node::INTERFACE_TYPE_DEFINITION => [ 'name', 'directives', 'fields' ], + Node::UNION_TYPE_DEFINITION => [ 'name', 'directives', 'types' ], + Node::ENUM_TYPE_DEFINITION => [ 'name', 'directives', 'values' ], + Node::ENUM_VALUE_DEFINITION => [ 'name', 'directives' ], + Node::INPUT_OBJECT_TYPE_DEFINITION => [ 'name', 'directives', 'fields' ], Node::TYPE_EXTENSION_DEFINITION => [ 'definition' ], Node::DIRECTIVE_DEFINITION => [ 'name', 'arguments', 'locations' ] ); diff --git a/tests/Language/ParserTest.php b/tests/Language/ParserTest.php index 35a8a4e..62791ab 100644 --- a/tests/Language/ParserTest.php +++ b/tests/Language/ParserTest.php @@ -7,6 +7,7 @@ use GraphQL\Language\AST\Field; use GraphQL\Language\AST\IntValue; use GraphQL\Language\AST\Location; use GraphQL\Language\AST\Name; +use GraphQL\Language\AST\Node; use GraphQL\Language\AST\OperationDefinition; use GraphQL\Language\AST\SelectionSet; use GraphQL\Language\AST\StringValue; @@ -18,45 +19,6 @@ use GraphQL\Utils; class ParserTest extends \PHPUnit_Framework_TestCase { - /** - * @it accepts option to not include source - */ - public function testAcceptsOptionToNotIncludeSource() - { - $actual = Parser::parse('{ field }', ['noSource' => true]); - - $expected = new Document([ - 'loc' => new Location(0, 9), - 'definitions' => [ - new OperationDefinition([ - 'loc' => new Location(0, 9), - 'operation' => 'query', - 'name' => null, - 'variableDefinitions' => null, - 'directives' => [], - 'selectionSet' => new SelectionSet([ - 'loc' => new Location(0, 9), - 'selections' => [ - new Field([ - 'loc' => new Location(2, 7), - 'alias' => null, - 'name' => new Name([ - 'loc' => new Location(2, 7), - 'value' => 'field' - ]), - 'arguments' => [], - 'directives' => [], - 'selectionSet' => null - ]) - ] - ]) - ]) - ] - ]); - - $this->assertEquals($expected, $actual); - } - /** * @it parse provides useful errors */ @@ -78,7 +40,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(0, '{', "Syntax Error GraphQL (1:2) Expected Name, found \n\n1: {\n ^\n", [1], [new SourceLocation(1,2)]); $run(1, '{ ...MissingOn } fragment MissingOn Type @@ -100,7 +62,7 @@ fragment MissingOn Type 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 {, found EOF\n\n1: query\n ^\n", $e->getMessage()); + $this->assertEquals("Syntax Error MyQuery.graphql (1:6) Expected {, found \n\n1: query\n ^\n", $e->getMessage()); } } @@ -287,7 +249,7 @@ fragment $fragmentName on Type { } /** - * @it parse creates ast + * @it creates ast */ public function testParseCreatesAst() { @@ -300,73 +262,267 @@ fragment $fragmentName on Type { '); $result = Parser::parse($source); - $expected = new Document(array( - 'loc' => new Location(0, 41, $source), - 'definitions' => array( - new OperationDefinition(array( - 'loc' => new Location(0, 40, $source), + $loc = function($start, $end) use ($source) { + return [ + 'start' => $start, + 'end' => $end + ]; + }; + + $expected = [ + 'kind' => Node::DOCUMENT, + 'loc' => $loc(0, 41), + 'definitions' => [ + [ + 'kind' => Node::OPERATION_DEFINITION, + 'loc' => $loc(0, 40), 'operation' => 'query', 'name' => null, 'variableDefinitions' => null, - 'directives' => array(), - 'selectionSet' => new SelectionSet(array( - 'loc' => new Location(0, 40, $source), - 'selections' => array( - new Field(array( - 'loc' => new Location(4, 38, $source), + 'directives' => [], + 'selectionSet' => [ + 'kind' => Node::SELECTION_SET, + 'loc' => $loc(0, 40), + 'selections' => [ + [ + 'kind' => Node::FIELD, + 'loc' => $loc(4, 38), 'alias' => null, - 'name' => new Name(array( - 'loc' => new Location(4, 8, $source), + 'name' => [ + 'kind' => Node::NAME, + 'loc' => $loc(4, 8), 'value' => 'node' - )), - 'arguments' => array( - new Argument(array( - 'name' => new Name(array( - 'loc' => new Location(9, 11, $source), + ], + 'arguments' => [ + [ + 'kind' => Node::ARGUMENT, + 'name' => [ + 'kind' => Node::NAME, + 'loc' => $loc(9, 11), 'value' => 'id' - )), - 'value' => new IntValue(array( - 'loc' => new Location(13, 14, $source), + ], + 'value' => [ + 'kind' => Node::INT, + 'loc' => $loc(13, 14), 'value' => '4' - )), - 'loc' => new Location(9, 14, $source) - )) - ), + ], + 'loc' => $loc(9, 14, $source) + ] + ], 'directives' => [], - 'selectionSet' => new SelectionSet(array( - 'loc' => new Location(16, 38, $source), - 'selections' => array( - new Field(array( - 'loc' => new Location(22, 24, $source), + 'selectionSet' => [ + 'kind' => Node::SELECTION_SET, + 'loc' => $loc(16, 38), + 'selections' => [ + [ + 'kind' => Node::FIELD, + 'loc' => $loc(22, 24), 'alias' => null, - 'name' => new Name(array( - 'loc' => new Location(22, 24, $source), + 'name' => [ + 'kind' => Node::NAME, + 'loc' => $loc(22, 24), 'value' => 'id' - )), + ], 'arguments' => [], 'directives' => [], 'selectionSet' => null - )), - new Field(array( - 'loc' => new Location(30, 34, $source), + ], + [ + 'kind' => Node::FIELD, + 'loc' => $loc(30, 34), 'alias' => null, - 'name' => new Name(array( - 'loc' => new Location(30, 34, $source), + 'name' => [ + 'kind' => Node::NAME, + 'loc' => $loc(30, 34), 'value' => 'name' - )), + ], 'arguments' => [], 'directives' => [], 'selectionSet' => null - )) - ) - )) - )) - ) - )) - )) - ) - )); + ] + ] + ] + ] + ] + ] + ] + ] + ]; - $this->assertEquals($expected, $result); + $this->assertEquals($expected, $this->nodeToArray($result)); + } + + /** + * @it allows parsing without source location information + */ + public function testAllowsParsingWithoutSourceLocationInformation() + { + $source = new Source('{ id }'); + $result = Parser::parse($source, ['noLocation' => true]); + + $this->assertEquals(null, $result->loc); + } + + /** + * @it contains location information that only stringifys start/end + */ + public function testConvertToArray() + { + $source = new Source('{ id }'); + $result = Parser::parse($source); + $this->assertEquals(['start' => 0, 'end' => '6'], TestUtils::locationToArray($result->loc)); + } + + /** + * @it contains references to source + */ + public function testContainsReferencesToSource() + { + $source = new Source('{ id }'); + $result = Parser::parse($source); + $this->assertEquals($source, $result->loc->source); + } + + /** + * @it contains references to start and end tokens + */ + public function testContainsReferencesToStartAndEndTokens() + { + $source = new Source('{ id }'); + $result = Parser::parse($source); + $this->assertEquals('', $result->loc->startToken->kind); + $this->assertEquals('', $result->loc->endToken->kind); + } + + // Describe: parseValue + + /** + * @it parses list values + */ + public function testParsesListValues() + { + $this->assertEquals([ + 'kind' => Node::LST, + 'loc' => ['start' => 0, 'end' => 11], + 'values' => [ + [ + 'kind' => Node::INT, + 'loc' => ['start' => 1, 'end' => 4], + 'value' => '123' + ], + [ + 'kind' => Node::STRING, + 'loc' => ['start' => 5, 'end' => 10], + 'value' => 'abc' + ] + ] + ], $this->nodeToArray(Parser::parseValue('[123 "abc"]'))); + } + + // Describe: parseType + + /** + * @it parses well known types + */ + public function testParsesWellKnownTypes() + { + $this->assertEquals([ + 'kind' => Node::NAMED_TYPE, + 'loc' => ['start' => 0, 'end' => 6], + 'name' => [ + 'kind' => Node::NAME, + 'loc' => ['start' => 0, 'end' => 6], + 'value' => 'String' + ] + ], $this->nodeToArray(Parser::parseType('String'))); + } + + /** + * @it parses custom types + */ + public function testParsesCustomTypes() + { + $this->assertEquals([ + 'kind' => Node::NAMED_TYPE, + 'loc' => ['start' => 0, 'end' => 6], + 'name' => [ + 'kind' => Node::NAME, + 'loc' => ['start' => 0, 'end' => 6], + 'value' => 'MyType' + ] + ], $this->nodeToArray(Parser::parseType('MyType'))); + } + + /** + * @it parses list types + */ + public function testParsesListTypes() + { + $this->assertEquals([ + 'kind' => Node::LIST_TYPE, + 'loc' => ['start' => 0, 'end' => 8], + 'type' => [ + 'kind' => Node::NAMED_TYPE, + 'loc' => ['start' => 1, 'end' => 7], + 'name' => [ + 'kind' => Node::NAME, + 'loc' => ['start' => 1, 'end' => 7], + 'value' => 'MyType' + ] + ] + ], $this->nodeToArray(Parser::parseType('[MyType]'))); + } + + /** + * @it parses non-null types + */ + public function testParsesNonNullTypes() + { + $this->assertEquals([ + 'kind' => Node::NON_NULL_TYPE, + 'loc' => ['start' => 0, 'end' => 7], + 'type' => [ + 'kind' => Node::NAMED_TYPE, + 'loc' => ['start' => 0, 'end' => 6], + 'name' => [ + 'kind' => Node::NAME, + 'loc' => ['start' => 0, 'end' => 6], + 'value' => 'MyType' + ] + ] + ], $this->nodeToArray(Parser::parseType('MyType!'))); + } + + /** + * @it parses nested types + */ + public function testParsesNestedTypes() + { + $this->assertEquals([ + 'kind' => Node::LIST_TYPE, + 'loc' => ['start' => 0, 'end' => 9], + 'type' => [ + 'kind' => Node::NON_NULL_TYPE, + 'loc' => ['start' => 1, 'end' => 8], + 'type' => [ + 'kind' => Node::NAMED_TYPE, + 'loc' => ['start' => 1, 'end' => 7], + 'name' => [ + 'kind' => Node::NAME, + 'loc' => ['start' => 1, 'end' => 7], + 'value' => 'MyType' + ] + ] + ] + ], $this->nodeToArray(Parser::parseType('[MyType!]'))); + } + + /** + * @param Node $node + * @return array + */ + public static function nodeToArray(Node $node) + { + return TestUtils::nodeToArray($node); } } diff --git a/tests/Language/SchemaParserTest.php b/tests/Language/SchemaParserTest.php index 6489fda..4a5b7d1 100644 --- a/tests/Language/SchemaParserTest.php +++ b/tests/Language/SchemaParserTest.php @@ -13,6 +13,7 @@ use GraphQL\Language\AST\ListType; use GraphQL\Language\AST\Location; use GraphQL\Language\AST\Name; use GraphQL\Language\AST\NamedType; +use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NonNullType; use GraphQL\Language\AST\ObjectTypeDefinition; use GraphQL\Language\AST\ScalarTypeDefinition; @@ -23,6 +24,8 @@ use GraphQL\Language\Source; class SchemaParserTest extends \PHPUnit_Framework_TestCase { + // Describe: Schema Parser + /** * @it Simple type */ @@ -33,13 +36,16 @@ type Hello { world: String }'; $doc = Parser::parse($body); - $loc = $this->createLocFn($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new ObjectTypeDefinition([ + [ + 'kind' => Node::OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(6, 11)), 'interfaces' => [], + 'directives' => [], 'fields' => [ $this->fieldNode( $this->nameNode('world', $loc(16, 21)), @@ -48,11 +54,11 @@ type Hello { ) ], 'loc' => $loc(1, 31) - ]) + ] ], - 'loc' => $loc(1, 31) - ]); - $this->assertEquals($doc, $expected); + 'loc' => $loc(0, 31) + ]; + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -63,15 +69,22 @@ type Hello { $body = ' extend type Hello { world: String -}'; +} +'; $doc = Parser::parse($body); - $loc = $this->createLocFn($body); - $expected = new Document([ + $loc = function($start, $end) { + return TestUtils::locArray($start, $end); + }; + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new TypeExtensionDefinition([ - 'definition' => new ObjectTypeDefinition([ + [ + 'kind' => Node::TYPE_EXTENSION_DEFINITION, + 'definition' => [ + 'kind' => Node::OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(13, 18)), 'interfaces' => [], + 'directives' => [], 'fields' => [ $this->fieldNode( $this->nameNode('world', $loc(23, 28)), @@ -80,13 +93,13 @@ extend type Hello { ) ], 'loc' => $loc(8, 38) - ]), + ], 'loc' => $loc(1, 38) - ]) + ] ], - 'loc' => $loc(1, 38) - ]); - $this->assertEquals($expected, $doc); + 'loc' => $loc(0, 39) + ]; + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -98,31 +111,37 @@ extend type Hello { type Hello { world: String! }'; - $loc = $this->createLocFn($body); + $loc = function($start, $end) { + return TestUtils::locArray($start, $end); + }; $doc = Parser::parse($body); - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new ObjectTypeDefinition([ + [ + 'kind' => Node::OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(6,11)), 'interfaces' => [], + 'directives' => [], 'fields' => [ $this->fieldNode( $this->nameNode('world', $loc(16, 21)), - new NonNullType([ + [ + 'kind' => Node::NON_NULL_TYPE, 'type' => $this->typeNode('String', $loc(23, 29)), 'loc' => $loc(23, 30) - ]), + ], $loc(16,30) ) ], 'loc' => $loc(1,32) - ]) + ] ], - 'loc' => $loc(1,32) - ]); + 'loc' => $loc(0,32) + ]; - $this->assertEquals($expected, $doc); + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -131,24 +150,27 @@ type Hello { public function testSimpleTypeInheritingInterface() { $body = 'type Hello implements World { }'; - $loc = $this->createLocFn($body); + $loc = function($start, $end) { return TestUtils::locArray($start, $end); }; $doc = Parser::parse($body); - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new ObjectTypeDefinition([ + [ + 'kind' => Node::OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(5, 10)), 'interfaces' => [ $this->typeNode('World', $loc(22, 27)) ], + 'directives' => [], 'fields' => [], 'loc' => $loc(0,31) - ]) + ] ], 'loc' => $loc(0,31) - ]); + ]; - $this->assertEquals($expected, $doc); + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -157,25 +179,28 @@ type Hello { public function testSimpleTypeInheritingMultipleInterfaces() { $body = 'type Hello implements Wo, rld { }'; - $loc = $this->createLocFn($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; $doc = Parser::parse($body); - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new ObjectTypeDefinition([ + [ + 'kind' => Node::OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(5, 10)), 'interfaces' => [ $this->typeNode('Wo', $loc(22,24)), $this->typeNode('rld', $loc(26,29)) ], + 'directives' => [], 'fields' => [], 'loc' => $loc(0, 33) - ]) + ] ], 'loc' => $loc(0, 33) - ]); + ]; - $this->assertEquals($expected, $doc); + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -184,21 +209,24 @@ type Hello { public function testSingleValueEnum() { $body = 'enum Hello { WORLD }'; - $loc = $this->createLocFn($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; $doc = Parser::parse($body); - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new EnumTypeDefinition([ + [ + 'kind' => Node::ENUM_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(5, 10)), + 'directives' => [], 'values' => [$this->enumValueNode('WORLD', $loc(13, 18))], 'loc' => $loc(0, 20) - ]) + ] ], 'loc' => $loc(0, 20) - ]); + ]; - $this->assertEquals($expected, $doc); + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -207,24 +235,27 @@ type Hello { public function testDoubleValueEnum() { $body = 'enum Hello { WO, RLD }'; - $loc = $this->createLocFn($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; $doc = Parser::parse($body); - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new EnumTypeDefinition([ + [ + 'kind' => Node::ENUM_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(5, 10)), + 'directives' => [], '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); + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -237,12 +268,15 @@ interface Hello { world: String }'; $doc = Parser::parse($body); - $loc = $this->createLocFn($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new InterfaceTypeDefinition([ + [ + 'kind' => Node::INTERFACE_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(11, 16)), + 'directives' => [], 'fields' => [ $this->fieldNode( $this->nameNode('world', $loc(21, 26)), @@ -251,11 +285,11 @@ interface Hello { ) ], 'loc' => $loc(1, 36) - ]) + ] ], - 'loc' => $loc(1,36) - ]); - $this->assertEquals($expected, $doc); + 'loc' => $loc(0,36) + ]; + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -268,13 +302,16 @@ type Hello { world(flag: Boolean): String }'; $doc = Parser::parse($body); - $loc = $this->createLocFn($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new ObjectTypeDefinition([ + [ + 'kind' => Node::OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(6, 11)), 'interfaces' => [], + 'directives' => [], 'fields' => [ $this->fieldNodeWithArgs( $this->nameNode('world', $loc(16, 21)), @@ -291,12 +328,12 @@ type Hello { ) ], 'loc' => $loc(1, 46) - ]) + ] ], - 'loc' => $loc(1, 46) - ]); + 'loc' => $loc(0, 46) + ]; - $this->assertEquals($expected, $doc); + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -309,13 +346,16 @@ type Hello { world(flag: Boolean = true): String }'; $doc = Parser::parse($body); - $loc = $this->createLocFn($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new ObjectTypeDefinition([ + [ + 'kind' => Node::OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(6, 11)), 'interfaces' => [], + 'directives' => [], 'fields' => [ $this->fieldNodeWithArgs( $this->nameNode('world', $loc(16, 21)), @@ -324,7 +364,7 @@ type Hello { $this->inputValueNode( $this->nameNode('flag', $loc(22, 26)), $this->typeNode('Boolean', $loc(28, 35)), - new BooleanValue(['value' => true, 'loc' => $loc(38, 42)]), + ['kind' => Node::BOOLEAN, 'value' => true, 'loc' => $loc(38, 42)], $loc(22, 42) ) ], @@ -332,11 +372,11 @@ type Hello { ) ], 'loc' => $loc(1, 53) - ]) + ] ], - 'loc' => $loc(1, 53) - ]); - $this->assertEquals($expected, $doc); + 'loc' => $loc(0, 53) + ]; + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -349,13 +389,16 @@ type Hello { world(things: [String]): String }'; $doc = Parser::parse($body); - $loc = $this->createLocFn($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new ObjectTypeDefinition([ + [ + 'kind' => Node::OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(6, 11)), 'interfaces' => [], + 'directives' => [], 'fields' => [ $this->fieldNodeWithArgs( $this->nameNode('world', $loc(16, 21)), @@ -363,7 +406,7 @@ type Hello { [ $this->inputValueNode( $this->nameNode('things', $loc(22,28)), - new ListType(['type' => $this->typeNode('String', $loc(31, 37)), 'loc' => $loc(30, 38)]), + ['kind' => Node::LIST_TYPE, 'type' => $this->typeNode('String', $loc(31, 37)), 'loc' => $loc(30, 38)], null, $loc(22, 38) ) @@ -372,12 +415,12 @@ type Hello { ) ], 'loc' => $loc(1, 49) - ]) + ] ], - 'loc' => $loc(1, 49) - ]); + 'loc' => $loc(0, 49) + ]; - $this->assertEquals($expected, $doc); + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -390,13 +433,16 @@ type Hello { world(argOne: Boolean, argTwo: Int): String }'; $doc = Parser::parse($body); - $loc = $this->createLocFn($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new ObjectTypeDefinition([ + [ + 'kind' => Node::OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(6, 11)), 'interfaces' => [], + 'directives' => [], 'fields' => [ $this->fieldNodeWithArgs( $this->nameNode('world', $loc(16, 21)), @@ -419,12 +465,12 @@ type Hello { ) ], 'loc' => $loc(1, 61) - ]) + ] ], - 'loc' => $loc(1, 61) - ]); + 'loc' => $loc(0, 61) + ]; - $this->assertEquals($expected, $doc); + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -434,19 +480,22 @@ type Hello { { $body = 'union Hello = World'; $doc = Parser::parse($body); - $loc = $this->createLocFn($body); - $expected = new Document([ + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new UnionTypeDefinition([ + [ + 'kind' => Node::UNION_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(6, 11)), + 'directives' => [], 'types' => [$this->typeNode('World', $loc(14, 19))], 'loc' => $loc(0, 19) - ]) + ] ], 'loc' => $loc(0, 19) - ]); + ]; - $this->assertEquals($expected, $doc); + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -456,22 +505,25 @@ type Hello { { $body = 'union Hello = Wo | Rld'; $doc = Parser::parse($body); - $loc = $this->createLocFn($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new UnionTypeDefinition([ + [ + 'kind' => Node::UNION_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(6, 11)), + 'directives' => [], '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); + ]; + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -481,17 +533,20 @@ type Hello { { $body = 'scalar Hello'; $doc = Parser::parse($body); - $loc = $this->createLocFn($body); - $expected = new Document([ + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new ScalarTypeDefinition([ + [ + 'kind' => Node::SCALAR_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(7, 12)), + 'directives' => [], 'loc' => $loc(0, 12) - ]) + ] ], 'loc' => $loc(0, 12) - ]); - $this->assertEquals($expected, $doc); + ]; + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -504,12 +559,15 @@ input Hello { world: String }'; $doc = Parser::parse($body); - $loc = $this->createLocFn($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; - $expected = new Document([ + $expected = [ + 'kind' => Node::DOCUMENT, 'definitions' => [ - new InputObjectTypeDefinition([ + [ + 'kind' => Node::INPUT_OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(7, 12)), + 'directives' => [], 'fields' => [ $this->inputValueNode( $this->nameNode('world', $loc(17, 22)), @@ -519,11 +577,11 @@ input Hello { ) ], 'loc' => $loc(1, 32) - ]) + ] ], - 'loc' => $loc(1, 32) - ]); - $this->assertEquals($expected, $doc); + 'loc' => $loc(0, 32) + ]; + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } /** @@ -539,28 +597,22 @@ input Hello { 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]), + return [ + 'kind' => Node::NAMED_TYPE, + 'name' => ['kind' => Node::NAME, 'value' => $name, 'loc' => $loc], 'loc' => $loc - ]); + ]; } private function nameNode($name, $loc) { - return new Name([ + return [ + 'kind' => Node::NAME, 'value' => $name, 'loc' => $loc - ]); + ]; } private function fieldNode($name, $type, $loc) @@ -570,29 +622,35 @@ input Hello { private function fieldNodeWithArgs($name, $type, $args, $loc) { - return new FieldDefinition([ + return [ + 'kind' => Node::FIELD_DEFINITION, 'name' => $name, 'arguments' => $args, 'type' => $type, + 'directives' => [], 'loc' => $loc - ]); + ]; } private function enumValueNode($name, $loc) { - return new EnumValueDefinition([ + return [ + 'kind' => Node::ENUM_VALUE_DEFINITION, 'name' => $this->nameNode($name, $loc), + 'directives' => [], 'loc' => $loc - ]); + ]; } private function inputValueNode($name, $type, $defaultValue, $loc) { - return new InputValueDefinition([ + return [ + 'kind' => Node::INPUT_VALUE_DEFINITION, 'name' => $name, 'type' => $type, 'defaultValue' => $defaultValue, + 'directives' => [], 'loc' => $loc - ]); + ]; } } diff --git a/tests/Language/SchemaPrinterTest.php b/tests/Language/SchemaPrinterTest.php index 792721c..c0ead91 100644 --- a/tests/Language/SchemaPrinterTest.php +++ b/tests/Language/SchemaPrinterTest.php @@ -65,29 +65,54 @@ type Foo implements Bar { six(argument: InputType = {key: "value"}): Type } +type AnnotatedObject @onObject(arg: "value") { + annotatedField(arg: Type = "default" @onArg): Type @onField +} + interface Bar { one: Type four(argument: String = "string"): String } +interface AnnotatedInterface @onInterface { + annotatedField(arg: Type @onArg): Type @onField +} + union Feed = Story | Article | Advert +union AnnotatedUnion @onUnion = A | B + scalar CustomScalar +scalar AnnotatedScalar @onScalar + enum Site { DESKTOP MOBILE } +enum AnnotatedEnum @onEnum { + ANNOTATED_VALUE @onEnumValue + OTHER_VALUE +} + input InputType { key: String! answer: Int = 42 } +input AnnotatedInput @onInputObjectType { + annotatedField: Type @onField +} + extend type Foo { seven(argument: [String]): Type } +extend type Foo @onType {} + +type NoFields {} + directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/tests/Language/TestUtils.php b/tests/Language/TestUtils.php new file mode 100644 index 0000000..96999ad --- /dev/null +++ b/tests/Language/TestUtils.php @@ -0,0 +1,64 @@ + $node->kind, + 'loc' => self::locationToArray($node->loc) + ]; + + foreach (get_object_vars($node) as $prop => $propValue) { + if (isset($result[$prop])) + continue; + + if (is_array($propValue)) { + $tmp = []; + foreach ($propValue as $tmp1) { + $tmp[] = $tmp1 instanceof Node ? self::nodeToArray($tmp1) : (array) $tmp1; + } + } else if ($propValue instanceof Node) { + $tmp = self::nodeToArray($propValue); + } else if (is_scalar($propValue) || null === $propValue) { + $tmp = $propValue; + } else { + $tmp = null; + } + + $result[$prop] = $tmp; + } + return $result; + } + + /** + * @param Location $loc + * @return array + */ + public static function locationToArray(Location $loc) + { + return [ + 'start' => $loc->start, + 'end' => $loc->end + ]; + } + + /** + * @param $start + * @param $end + * @return array + */ + public static function locArray($start, $end) + { + return ['start' => $start, 'end' => $end]; + } +} diff --git a/tests/Language/schema-kitchen-sink.graphql b/tests/Language/schema-kitchen-sink.graphql index e623ec4..d148276 100644 --- a/tests/Language/schema-kitchen-sink.graphql +++ b/tests/Language/schema-kitchen-sink.graphql @@ -19,29 +19,54 @@ type Foo implements Bar { six(argument: InputType = {key: "value"}): Type } +type AnnotatedObject @onObject(arg: "value") { + annotatedField(arg: Type = "default" @onArg): Type @onField +} + interface Bar { one: Type four(argument: String = "string"): String } +interface AnnotatedInterface @onInterface { + annotatedField(arg: Type @onArg): Type @onField +} + union Feed = Story | Article | Advert +union AnnotatedUnion @onUnion = A | B + scalar CustomScalar +scalar AnnotatedScalar @onScalar + enum Site { DESKTOP MOBILE } +enum AnnotatedEnum @onEnum { + ANNOTATED_VALUE @onEnumValue + OTHER_VALUE +} + input InputType { key: String! answer: Int = 42 } +input AnnotatedInput @onInputObjectType { + annotatedField: Type @onField +} + extend type Foo { seven(argument: [String]): Type } +extend type Foo @onType {} + +type NoFields {} + directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include(if: Boolean!)