diff --git a/src/Language/AST/Builder/AbstractBuilder.php b/src/Language/AST/Builder/AbstractBuilder.php index c0ce7fcf..efeb3f9a 100644 --- a/src/Language/AST/Builder/AbstractBuilder.php +++ b/src/Language/AST/Builder/AbstractBuilder.php @@ -45,7 +45,7 @@ protected function createLocation(array $ast): Location * @param null $defaultValue * @return mixed|null */ - protected function getOne(array $ast, string $propertyName, $defaultValue = null) + protected function get(array $ast, string $propertyName, $defaultValue = null) { return $ast[$propertyName] ?? $defaultValue; } diff --git a/src/Language/AST/Builder/BooleanBuilder.php b/src/Language/AST/Builder/BooleanBuilder.php index 8f93c19e..6f8bfb6b 100644 --- a/src/Language/AST/Builder/BooleanBuilder.php +++ b/src/Language/AST/Builder/BooleanBuilder.php @@ -15,7 +15,7 @@ class BooleanBuilder extends AbstractBuilder public function build(array $ast): NodeInterface { return new BooleanValueNode([ - 'value' => $this->getOne($ast, 'value'), + 'value' => $this->get($ast, 'value'), 'location' => $this->createLocation($ast), ]); } diff --git a/src/Language/AST/Builder/EnumBuilder.php b/src/Language/AST/Builder/EnumBuilder.php index 7ecb2542..97ebe690 100644 --- a/src/Language/AST/Builder/EnumBuilder.php +++ b/src/Language/AST/Builder/EnumBuilder.php @@ -16,7 +16,7 @@ class EnumBuilder extends AbstractBuilder public function build(array $ast): NodeInterface { return new EnumValueNode([ - 'value' => $this->getOne($ast, 'value'), + 'value' => $this->get($ast, 'value'), 'location' => $this->createLocation($ast), ]); } diff --git a/src/Language/AST/Builder/FieldDefinitionBuilder.php b/src/Language/AST/Builder/FieldDefinitionBuilder.php new file mode 100644 index 00000000..adf8288b --- /dev/null +++ b/src/Language/AST/Builder/FieldDefinitionBuilder.php @@ -0,0 +1,34 @@ + $this->buildOne($ast, 'description'), + 'name' => $this->buildOne($ast, 'name'), + 'arguments' => $this->buildMany($ast, 'arguments'), + 'type' => $this->buildOne($ast, 'type'), + 'directives' => $this->buildMany($ast, 'directives'), + 'location' => $this->createLocation($ast), + ]); + } + + /** + * @inheritdoc + */ + public function supportsKind(string $kind): bool + { + return $kind === NodeKindEnum::FIELD_DEFINITION; + } +} diff --git a/src/Language/AST/Builder/FloatBuilder.php b/src/Language/AST/Builder/FloatBuilder.php index 23718669..c78301a8 100644 --- a/src/Language/AST/Builder/FloatBuilder.php +++ b/src/Language/AST/Builder/FloatBuilder.php @@ -15,7 +15,7 @@ class FloatBuilder extends AbstractBuilder public function build(array $ast): NodeInterface { return new FloatValueNode([ - 'value' => $this->getOne($ast, 'value'), + 'value' => $this->get($ast, 'value'), 'location' => $this->createLocation($ast), ]); } diff --git a/src/Language/AST/Builder/IntBuilder.php b/src/Language/AST/Builder/IntBuilder.php index 8ee700c5..523c057e 100644 --- a/src/Language/AST/Builder/IntBuilder.php +++ b/src/Language/AST/Builder/IntBuilder.php @@ -15,7 +15,7 @@ class IntBuilder extends AbstractBuilder public function build(array $ast): NodeInterface { return new IntValueNode([ - 'value' => $this->getOne($ast, 'value'), + 'value' => $this->get($ast, 'value'), 'location' => $this->createLocation($ast), ]); } diff --git a/src/Language/AST/Builder/NameBuilder.php b/src/Language/AST/Builder/NameBuilder.php index 03af648a..fb9ff491 100644 --- a/src/Language/AST/Builder/NameBuilder.php +++ b/src/Language/AST/Builder/NameBuilder.php @@ -15,7 +15,7 @@ class NameBuilder extends AbstractBuilder public function build(array $ast): NodeInterface { return new NameNode([ - 'value' => $this->getOne($ast, 'value'), + 'value' => $this->get($ast, 'value'), 'location' => $this->createLocation($ast), ]); } diff --git a/src/Language/AST/Builder/ObjectTypeDefinitionBuilder.php b/src/Language/AST/Builder/ObjectTypeDefinitionBuilder.php new file mode 100644 index 00000000..a536e1d3 --- /dev/null +++ b/src/Language/AST/Builder/ObjectTypeDefinitionBuilder.php @@ -0,0 +1,34 @@ + $this->buildOne($ast, 'description'), + 'name' => $this->buildOne($ast, 'name'), + 'interfaces' => $this->buildMany($ast, 'interfaces'), + 'directives' => $this->buildMany($ast, 'directives'), + 'fields' => $this->buildMany($ast, 'fields'), + 'location' => $this->createLocation($ast), + ]); + } + + /** + * @inheritdoc + */ + public function supportsKind(string $kind): bool + { + return $kind === NodeKindEnum::OBJECT_TYPE_DEFINITION; + } +} diff --git a/src/Language/AST/Builder/OperationDefinitionBuilder.php b/src/Language/AST/Builder/OperationDefinitionBuilder.php index 82131f75..68304dd1 100644 --- a/src/Language/AST/Builder/OperationDefinitionBuilder.php +++ b/src/Language/AST/Builder/OperationDefinitionBuilder.php @@ -15,7 +15,7 @@ class OperationDefinitionBuilder extends AbstractBuilder public function build(array $ast): NodeInterface { return new OperationDefinitionNode([ - 'operation' => $this->getOne($ast, 'operation'), + 'operation' => $this->get($ast, 'operation'), 'variableDefinitions' => $this->buildMany($ast, 'variableDefinitions'), 'directives' => $this->buildMany($ast, 'directives'), 'selectionSet' => $this->buildOne($ast, 'selectionSet'), diff --git a/src/Language/AST/Builder/StringBuilder.php b/src/Language/AST/Builder/StringBuilder.php index 76a38c44..f36f5627 100644 --- a/src/Language/AST/Builder/StringBuilder.php +++ b/src/Language/AST/Builder/StringBuilder.php @@ -15,8 +15,8 @@ class StringBuilder extends AbstractBuilder public function build(array $ast): NodeInterface { return new StringValueNode([ - 'value' => $this->getOne($ast, 'value'), - 'block' => $this->getOne($ast, 'block', false), + 'value' => $this->get($ast, 'value'), + 'block' => $this->get($ast, 'block', false), 'location' => $this->createLocation($ast), ]); } diff --git a/src/Language/AST/DirectiveLocationEnum.php b/src/Language/AST/DirectiveLocationEnum.php index 2c4ddf25..c9716ac8 100644 --- a/src/Language/AST/DirectiveLocationEnum.php +++ b/src/Language/AST/DirectiveLocationEnum.php @@ -25,4 +25,13 @@ class DirectiveLocationEnum const ENUM_VALUE = 'ENUM_VALUE'; const INPUT_OBJECT = 'INPUT_OBJECT'; const INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION'; + + /** + * @return array + * @throws \ReflectionException + */ + public static function values(): array + { + return array_values((new \ReflectionClass(__CLASS__))->getConstants()); + } } diff --git a/src/Language/AST/Node/Behavior/DescriptionTrait.php b/src/Language/AST/Node/Behavior/DescriptionTrait.php index 99c3dd55..e909ea22 100644 --- a/src/Language/AST/Node/Behavior/DescriptionTrait.php +++ b/src/Language/AST/Node/Behavior/DescriptionTrait.php @@ -8,15 +8,23 @@ trait DescriptionTrait { /** - * @var ?StringValueNode + * @var StringValueNode|null */ protected $description; /** - * @return StringValueNode + * @return StringValueNode|null */ public function getDescription(): ?StringValueNode { return $this->description; } + + /** + * @return array|null + */ + public function getDescriptionAsArray(): ?array + { + return null !== $this->description ? $this->description->toArray() : null; + } } diff --git a/src/Language/AST/Node/Behavior/FieldsTrait.php b/src/Language/AST/Node/Behavior/FieldsTrait.php index 09490118..3d940335 100644 --- a/src/Language/AST/Node/Behavior/FieldsTrait.php +++ b/src/Language/AST/Node/Behavior/FieldsTrait.php @@ -2,6 +2,7 @@ namespace Digia\GraphQL\Language\AST\Node\Behavior; +use Digia\GraphQL\Contract\SerializationInterface; use Digia\GraphQL\Language\AST\Node\FieldDefinitionNode; trait FieldsTrait @@ -19,4 +20,14 @@ public function getFields(): array { return $this->fields; } + + /** + * @return array + */ + public function getFieldsAsArray(): array + { + return array_map(function (SerializationInterface $node) { + return $node->toArray(); + }, $this->fields); + } } diff --git a/src/Language/AST/Node/Behavior/InterfacesTrait.php b/src/Language/AST/Node/Behavior/InterfacesTrait.php index 62b7336d..5f64b0d8 100644 --- a/src/Language/AST/Node/Behavior/InterfacesTrait.php +++ b/src/Language/AST/Node/Behavior/InterfacesTrait.php @@ -19,4 +19,13 @@ public function getInterfaces(): array { return $this->interfaces; } + + /** + * @return array + */ + public function getInterfacesAsArray(): array + { + // TODO: Implement this method. + return []; + } } diff --git a/src/Language/AST/Node/DocumentNode.php b/src/Language/AST/Node/DocumentNode.php index 771bf91e..dbdec16a 100644 --- a/src/Language/AST/Node/DocumentNode.php +++ b/src/Language/AST/Node/DocumentNode.php @@ -45,8 +45,8 @@ public function toArray(): array { return [ 'kind' => $this->kind, - 'loc' => $this->getLocationAsArray(), 'definitions' => $this->getDefinitionsAsArray(), + 'loc' => $this->getLocationAsArray(), ]; } } diff --git a/src/Language/AST/Node/FieldDefinitionNode.php b/src/Language/AST/Node/FieldDefinitionNode.php index d16490ce..048051b9 100644 --- a/src/Language/AST/Node/FieldDefinitionNode.php +++ b/src/Language/AST/Node/FieldDefinitionNode.php @@ -2,18 +2,20 @@ namespace Digia\GraphQL\Language\AST\Node; -use Digia\GraphQL\Language\AST\NodeKindEnum; +use Digia\GraphQL\Language\AST\Node\Behavior\ArgumentsTrait; use Digia\GraphQL\Language\AST\Node\Behavior\DescriptionTrait; use Digia\GraphQL\Language\AST\Node\Behavior\DirectivesTrait; use Digia\GraphQL\Language\AST\Node\Behavior\NameTrait; use Digia\GraphQL\Language\AST\Node\Behavior\TypeTrait; use Digia\GraphQL\Language\AST\Node\Contract\DefinitionNodeInterface; +use Digia\GraphQL\Language\AST\NodeKindEnum; class FieldDefinitionNode extends AbstractNode implements DefinitionNodeInterface { use DescriptionTrait; use NameTrait; + use ArgumentsTrait; use TypeTrait; use DirectivesTrait; @@ -23,15 +25,18 @@ class FieldDefinitionNode extends AbstractNode implements DefinitionNodeInterfac protected $kind = NodeKindEnum::FIELD_DEFINITION; /** - * @var InputValueDefinitionNode[] - */ - protected $arguments; - - /** - * @return InputValueDefinitionNode[] + * @inheritdoc */ - public function getArguments(): array + public function toArray(): array { - return $this->arguments; + return [ + 'kind' => $this->kind, + 'description' => $this->description, + 'name' => $this->getNameAsArray(), + 'arguments' => $this->getArgumentsAsArray(), + 'type' => $this->getTypeAsArray(), + 'directives' => $this->getDirectivesAsArray(), + 'loc' => $this->getLocationAsArray(), + ]; } } diff --git a/src/Language/AST/Node/NameNode.php b/src/Language/AST/Node/NameNode.php index 04e26c97..ef8029b6 100644 --- a/src/Language/AST/Node/NameNode.php +++ b/src/Language/AST/Node/NameNode.php @@ -23,8 +23,8 @@ public function toArray(): array { return [ 'kind' => $this->kind, - 'loc' => $this->getLocationAsArray(), 'value' => $this->value, + 'loc' => $this->getLocationAsArray(), ]; } } diff --git a/src/Language/AST/Node/NamedTypeNode.php b/src/Language/AST/Node/NamedTypeNode.php index 70254522..2acf5ffc 100644 --- a/src/Language/AST/Node/NamedTypeNode.php +++ b/src/Language/AST/Node/NamedTypeNode.php @@ -23,8 +23,8 @@ public function toArray(): array { return [ 'kind' => $this->kind, - 'loc' => $this->getLocationAsArray(), 'name' => $this->getNameAsArray(), + 'loc' => $this->getLocationAsArray(), ]; } } diff --git a/src/Language/AST/Node/ObjectTypeDefinitionNode.php b/src/Language/AST/Node/ObjectTypeDefinitionNode.php index 8a78e98f..119ee286 100644 --- a/src/Language/AST/Node/ObjectTypeDefinitionNode.php +++ b/src/Language/AST/Node/ObjectTypeDefinitionNode.php @@ -2,13 +2,13 @@ namespace Digia\GraphQL\Language\AST\Node; -use Digia\GraphQL\Language\AST\NodeKindEnum; use Digia\GraphQL\Language\AST\Node\Behavior\DescriptionTrait; use Digia\GraphQL\Language\AST\Node\Behavior\DirectivesTrait; use Digia\GraphQL\Language\AST\Node\Behavior\FieldsTrait; use Digia\GraphQL\Language\AST\Node\Behavior\InterfacesTrait; use Digia\GraphQL\Language\AST\Node\Behavior\NameTrait; use Digia\GraphQL\Language\AST\Node\Contract\TypeDefinitionNodeInterface; +use Digia\GraphQL\Language\AST\NodeKindEnum; class ObjectTypeDefinitionNode extends AbstractNode implements TypeDefinitionNodeInterface { @@ -23,4 +23,21 @@ class ObjectTypeDefinitionNode extends AbstractNode implements TypeDefinitionNod * @var string */ protected $kind = NodeKindEnum::OBJECT_TYPE_DEFINITION; + + /** + * @inheritdoc + */ + public function toArray(): array + { + return [ + 'kind' => $this->kind, + 'description' => $this->getDescriptionAsArray(), + 'name' => $this->getNameAsArray(), + 'interfaces' => $this->getInterfacesAsArray(), + 'directives' => $this->getDirectivesAsArray(), + 'fields' => $this->getFieldsAsArray(), + 'loc' => $this->getLocationAsArray(), + ]; + } + } diff --git a/src/Language/ASTParser.php b/src/Language/ASTParser.php index 9b0cd923..ef9a7a36 100644 --- a/src/Language/ASTParser.php +++ b/src/Language/ASTParser.php @@ -6,8 +6,10 @@ use Digia\GraphQL\Error\SyntaxError; use Digia\GraphQL\Language\AST\Builder\Contract\BuilderInterface; use Digia\GraphQL\Language\AST\Builder\Contract\DirectorInterface; +use Digia\GraphQL\Language\AST\DirectiveLocationEnum; use Digia\GraphQL\Language\AST\Node\Contract\NodeInterface; use Digia\GraphQL\Language\AST\NodeKindEnum; +use Digia\GraphQL\Language\Contract\LexerInterface; use Digia\GraphQL\Language\Contract\ParserInterface; use Digia\GraphQL\Language\Reader\Contract\ReaderInterface; @@ -155,9 +157,9 @@ protected function getBuilder(string $kind): ?BuilderInterface /** * @param Source $source * @param array $options - * @return Lexer + * @return LexerInterface */ - protected function createLexer(Source $source, array $options): Lexer + protected function createLexer(Source $source, array $options): LexerInterface { return new Lexer($source, $this->readers, $options); } @@ -165,25 +167,25 @@ protected function createLexer(Source $source, array $options): Lexer /** * Determines if the next token is of a given kind. * - * @param Lexer $lexer + * @param LexerInterface $lexer * @param string $kind * @return bool */ - protected function peek(Lexer $lexer, string $kind): bool + protected function peek(LexerInterface $lexer, string $kind): bool { - return $lexer->getToken()->getKind() === $kind; + return $lexer->getTokenKind() === $kind; } /** * If the next token is of the given kind, return true after advancing * the lexer. Otherwise, do not change the parser state and return false. * - * @param Lexer $lexer + * @param LexerInterface $lexer * @param string $kind * @return bool * @throws GraphQLError */ - protected function skip(Lexer $lexer, string $kind): bool + protected function skip(LexerInterface $lexer, string $kind): bool { if ($match = $this->peek($lexer, $kind)) { $lexer->advance(); @@ -196,12 +198,12 @@ protected function skip(Lexer $lexer, string $kind): bool * If the next token is of the given kind, return that token after advancing * the lexer. Otherwise, do not change the parser state and throw an error. * - * @param Lexer $lexer + * @param LexerInterface $lexer * @param string $kind * @return Token * @throws GraphQLError */ - protected function expect(Lexer $lexer, string $kind): Token + protected function expect(LexerInterface $lexer, string $kind): Token { $token = $lexer->getToken(); @@ -214,12 +216,12 @@ protected function expect(Lexer $lexer, string $kind): Token } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @param string $value * @return Token * @throws GraphQLError */ - protected function expectKeyword(Lexer $lexer, string $value): Token + protected function expectKeyword(LexerInterface $lexer, string $value): Token { $token = $lexer->getToken(); @@ -235,11 +237,11 @@ protected function expectKeyword(Lexer $lexer, string $value): Token * Helper function for creating an error when an unexpected lexed token * is encountered. * - * @param Lexer $lexer + * @param LexerInterface $lexer * @param Token|null $atToken * @return GraphQLError */ - protected function unexpected(Lexer $lexer, ?Token $atToken = null): GraphQLError + protected function unexpected(LexerInterface $lexer, ?Token $atToken = null): GraphQLError { $token = $atToken ?: $lexer->getToken(); @@ -247,11 +249,11 @@ protected function unexpected(Lexer $lexer, ?Token $atToken = null): GraphQLErro } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @param Token $startToken * @return array */ - protected function createLocation(Lexer $lexer, Token $startToken): array + protected function createLocation(LexerInterface $lexer, Token $startToken): array { return [ 'start' => $startToken->getStart(), @@ -265,14 +267,14 @@ protected function createLocation(Lexer $lexer, Token $startToken): array * and ends with a lex token of closeKind. Advances the parser * to the next lex token after the closing token. * - * @param Lexer $lexer + * @param LexerInterface $lexer * @param string $openKind * @param callable $parseFunction * @param string $closeKind * @return array * @throws GraphQLError */ - protected function any(Lexer $lexer, string $openKind, callable $parseFunction, string $closeKind): array + protected function any(LexerInterface $lexer, string $openKind, callable $parseFunction, string $closeKind): array { $this->expect($lexer, $openKind); @@ -291,14 +293,14 @@ protected function any(Lexer $lexer, string $openKind, callable $parseFunction, * and ends with a lex token of closeKind. Advances the parser * to the next lex token after the closing token. * - * @param Lexer $lexer + * @param LexerInterface $lexer * @param string $openKind * @param callable $parseFunction * @param string $closeKind * @return array * @throws GraphQLError */ - protected function many(Lexer $lexer, string $openKind, callable $parseFunction, string $closeKind): array + protected function many(LexerInterface $lexer, string $openKind, callable $parseFunction, string $closeKind): array { $this->expect($lexer, $openKind); @@ -312,11 +314,11 @@ protected function many(Lexer $lexer, string $openKind, callable $parseFunction, } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseName(Lexer $lexer): array + protected function parseName(LexerInterface $lexer): array { $token = $this->expect($lexer, TokenKindEnum::NAME); @@ -328,11 +330,11 @@ protected function parseName(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseDocument(Lexer $lexer): array + protected function parseDocument(LexerInterface $lexer): array { $start = $lexer->getToken(); @@ -352,57 +354,58 @@ protected function parseDocument(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError + * @throws \ReflectionException */ - protected function parseDefinition(Lexer $lexer): array + protected function parseDefinition(LexerInterface $lexer): array { // TODO: Consider adding experimental support for parsing schema definitions if ($this->peek($lexer, TokenKindEnum::NAME)) { - switch ($lexer->getToken()->getValue()) { - case 'query': - case 'mutation': - case 'subscription': - case 'fragment': + switch ($lexer->getTokenValue()) { + case KeywordEnum::QUERY: + case KeywordEnum::MUTATION: + case KeywordEnum::SUBSCRIPTION: + case KeywordEnum::FRAGMENT: return $this->parseExecutableDefinition($lexer); - case 'schema': - case 'scalar': - case 'type': - case 'interface': - case 'union': - case 'enum': - case 'input': - case 'extend': - case 'directive': + case KeywordEnum::SCHEMA: + case KeywordEnum::SCALAR: + case KeywordEnum::TYPE: + case KeywordEnum::INTERFACE: + case KeywordEnum::UNION: + case KeywordEnum::ENUM: + case KeywordEnum::INPUT: + case KeywordEnum::EXTEND: + case KeywordEnum::DIRECTIVE: // Note: The schema definition language is an experimental addition. -// return $this->parseTypeSystemDefinition($lexer); + return $this->parseTypeSystemDefinition($lexer); } } elseif ($this->peek($lexer, TokenKindEnum::BRACE_L)) { return $this->parseExecutableDefinition($lexer); -// } elseif ($this->peekDescription($lexer)) { -// // Note: The schema definition language is an experimental addition. -// return $this->parseTypeSystemDefinition($lexer); + } elseif ($this->peekDescription($lexer)) { + // Note: The schema definition language is an experimental addition. + return $this->parseTypeSystemDefinition($lexer); } throw $this->unexpected($lexer); } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseExecutableDefinition(Lexer $lexer): array + protected function parseExecutableDefinition(LexerInterface $lexer): array { if ($this->peek($lexer, TokenKindEnum::NAME)) { switch ($lexer->getToken()->getValue()) { - case 'query': - case 'mutation': - case 'subscription': + case KeywordEnum::QUERY: + case KeywordEnum::MUTATION: + case KeywordEnum::SUBSCRIPTION: return $this->parseOperationDefinition($lexer); - case 'fragment': + case KeywordEnum::FRAGMENT: return $this->parseFragmentDefinition($lexer); } } elseif ($this->peek($lexer, TokenKindEnum::BRACE_L)) { @@ -413,11 +416,11 @@ protected function parseExecutableDefinition(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseOperationDefinition(Lexer $lexer): array + protected function parseOperationDefinition(LexerInterface $lexer): array { $start = $lexer->getToken(); @@ -451,11 +454,28 @@ protected function parseOperationDefinition(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer + * @return string + * @throws GraphQLError + */ + protected function parseOperationType(LexerInterface $lexer): string + { + $token = $this->expect($lexer, TokenKindEnum::NAME); + $value = $token->getValue(); + + if (isOperation($value)) { + return $value; + } + + throw $this->unexpected($lexer, $token); + } + + /** + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseVariableDefinitions(Lexer $lexer): array + protected function parseVariableDefinitions(LexerInterface $lexer): array { return $this->peek($lexer, TokenKindEnum::PAREN_L) ? $this->many($lexer, TokenKindEnum::PAREN_L, [$this, 'parseVariableDefinition'], TokenKindEnum::PAREN_R) @@ -463,20 +483,20 @@ protected function parseVariableDefinitions(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseVariableDefinition(Lexer $lexer): array + protected function parseVariableDefinition(LexerInterface $lexer): array { $start = $lexer->getToken(); /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return mixed * @throws GraphQLError */ - $parseType = function (Lexer $lexer) { + $parseType = function (LexerInterface $lexer) { $this->expect($lexer, TokenKindEnum::COLON); return $this->parseTypeReference($lexer); }; @@ -493,11 +513,11 @@ protected function parseVariableDefinition(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseVariable(Lexer $lexer): array + protected function parseVariable(LexerInterface $lexer): array { $start = $lexer->getToken(); @@ -511,11 +531,11 @@ protected function parseVariable(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseSelectionSet(Lexer $lexer): array + protected function parseSelectionSet(LexerInterface $lexer): array { $start = $lexer->getToken(); @@ -532,11 +552,11 @@ protected function parseSelectionSet(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseSelection(Lexer $lexer): array + protected function parseSelection(LexerInterface $lexer): array { return $this->peek($lexer, TokenKindEnum::SPREAD) ? $this->parseFragment($lexer) @@ -544,11 +564,11 @@ protected function parseSelection(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseField(Lexer $lexer): array + protected function parseField(LexerInterface $lexer): array { $start = $lexer->getToken(); @@ -575,12 +595,12 @@ protected function parseField(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @param bool $isConst * @return array * @throws GraphQLError */ - protected function parseArguments(Lexer $lexer, bool $isConst): array + protected function parseArguments(LexerInterface $lexer, bool $isConst): array { return $this->peek($lexer, TokenKindEnum::PAREN_L) ? $this->many( @@ -593,20 +613,20 @@ protected function parseArguments(Lexer $lexer, bool $isConst): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseArgument(Lexer $lexer): array + protected function parseArgument(LexerInterface $lexer): array { $start = $lexer->getToken(); /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return mixed * @throws GraphQLError */ - $parseValue = function (Lexer $lexer) { + $parseValue = function (LexerInterface $lexer) { $this->expect($lexer, TokenKindEnum::COLON); return $this->parseValueLiteral($lexer, false); }; @@ -620,20 +640,20 @@ protected function parseArgument(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseConstArgument(Lexer $lexer): array + protected function parseConstArgument(LexerInterface $lexer): array { $start = $lexer->getToken(); /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return mixed * @throws GraphQLError */ - $parseValue = function (Lexer $lexer) { + $parseValue = function (LexerInterface $lexer) { $this->expect($lexer, TokenKindEnum::COLON); return $this->parseConstValue($lexer); }; @@ -647,17 +667,17 @@ protected function parseConstArgument(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseFragment(Lexer $lexer): array + protected function parseFragment(LexerInterface $lexer): array { $start = $lexer->getToken(); $this->expect($lexer, TokenKindEnum::SPREAD); - $tokenValue = $lexer->getToken()->getValue(); + $tokenValue = $lexer->getTokenValue(); if ($this->peek($lexer, TokenKindEnum::NAME) && $tokenValue !== 'on') { return [ @@ -684,19 +704,19 @@ protected function parseFragment(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseFragmentDefinition(Lexer $lexer): array + protected function parseFragmentDefinition(LexerInterface $lexer): array { $start = $lexer->getToken(); - $this->expectKeyword($lexer, 'fragment'); + $this->expectKeyword($lexer, KeywordEnum::FRAGMENT); // TODO: Consider adding experimental support fragment variables - $parseTypeCondition = function (Lexer $lexer) { + $parseTypeCondition = function (LexerInterface $lexer) { $this->expectKeyword($lexer, 'on'); return $this->parseNamedType($lexer); }; @@ -712,13 +732,13 @@ protected function parseFragmentDefinition(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseFragmentName(Lexer $lexer): array + protected function parseFragmentName(LexerInterface $lexer): array { - if ($lexer->getToken()->getValue() === 'on') { + if ($lexer->getTokenValue() === 'on') { throw $this->unexpected($lexer); } @@ -726,12 +746,12 @@ protected function parseFragmentName(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @param bool $isConst * @return array * @throws GraphQLError */ - protected function parseValueLiteral(Lexer $lexer, bool $isConst): array + protected function parseValueLiteral(LexerInterface $lexer, bool $isConst): array { $token = $lexer->getToken(); @@ -797,11 +817,11 @@ protected function parseValueLiteral(Lexer $lexer, bool $isConst): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseStringLiteral(Lexer $lexer): array + protected function parseStringLiteral(LexerInterface $lexer): array { $token = $lexer->getToken(); @@ -816,32 +836,32 @@ protected function parseStringLiteral(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseConstValue(Lexer $lexer): array + protected function parseConstValue(LexerInterface $lexer): array { return $this->parseValueLiteral($lexer, true); } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseValueValue(Lexer $lexer): array + protected function parseValueValue(LexerInterface $lexer): array { return $this->parseValueLiteral($lexer, false); } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @param bool $isConst * @return array * @throws GraphQLError */ - protected function parseList(Lexer $lexer, bool $isConst): array + protected function parseList(LexerInterface $lexer, bool $isConst): array { $start = $lexer->getToken(); @@ -858,12 +878,12 @@ protected function parseList(Lexer $lexer, bool $isConst): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @param bool $isConst * @return array * @throws GraphQLError */ - protected function parseObject(Lexer $lexer, bool $isConst): array + protected function parseObject(LexerInterface $lexer, bool $isConst): array { $start = $lexer->getToken(); @@ -883,16 +903,16 @@ protected function parseObject(Lexer $lexer, bool $isConst): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @param bool $isConst * @return array * @throws GraphQLError */ - protected function parseObjectField(Lexer $lexer, bool $isConst): array + protected function parseObjectField(LexerInterface $lexer, bool $isConst): array { $start = $lexer->getToken(); - $parseValue = function (Lexer $lexer, bool $isConst) { + $parseValue = function (LexerInterface $lexer, bool $isConst) { $this->expect($lexer, TokenKindEnum::COLON); return $this->parseValueLiteral($lexer, $isConst); }; @@ -906,12 +926,12 @@ protected function parseObjectField(Lexer $lexer, bool $isConst): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @param bool $isConst * @return array * @throws GraphQLError */ - protected function parseDirectives(Lexer $lexer, bool $isConst): array + protected function parseDirectives(LexerInterface $lexer, bool $isConst): array { $directives = []; @@ -923,12 +943,12 @@ protected function parseDirectives(Lexer $lexer, bool $isConst): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @param bool $isConst * @return array * @throws GraphQLError */ - protected function parseDirective(Lexer $lexer, bool $isConst): array + protected function parseDirective(LexerInterface $lexer, bool $isConst): array { $start = $lexer->getToken(); @@ -943,11 +963,11 @@ protected function parseDirective(Lexer $lexer, bool $isConst): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseTypeReference(Lexer $lexer): array + protected function parseTypeReference(LexerInterface $lexer): array { $start = $lexer->getToken(); @@ -977,11 +997,11 @@ protected function parseTypeReference(Lexer $lexer): array } /** - * @param Lexer $lexer + * @param LexerInterface $lexer * @return array * @throws GraphQLError */ - protected function parseNamedType(Lexer $lexer): array + protected function parseNamedType(LexerInterface $lexer): array { $start = $lexer->getToken(); @@ -993,19 +1013,716 @@ protected function parseNamedType(Lexer $lexer): array } /** - * @param Lexer $lexer - * @return string + * @param LexerInterface $lexer + * @return array * @throws GraphQLError + * @throws \ReflectionException */ - protected function parseOperationType(Lexer $lexer): string + protected function parseTypeSystemDefinition(LexerInterface $lexer): array { - $token = $this->expect($lexer, TokenKindEnum::NAME); - $value = $token->getValue(); + // Many definitions begin with a description and require a lookahead. + $keywordToken = $this->peekDescription($lexer) ? $lexer->lookahead() : $lexer->getToken(); + + if ($keywordToken->getKind() === TokenKindEnum::NAME) { + switch ($keywordToken->getValue()) { + case KeywordEnum::SCHEMA: + return $this->parseSchemaDefinition($lexer); + case KeywordEnum::SCALAR: + return $this->parseScalarTypeDefinition($lexer); + case KeywordEnum::TYPE: + return $this->parseObjectTypeDefinition($lexer); + case KeywordEnum::INTERFACE: + return $this->parseInterfaceTypeDefinition($lexer); + case KeywordEnum::UNION: + return $this->parseUnionTypeDefinition($lexer); + case KeywordEnum::ENUM: + return $this->parseEnumTypeDefinition($lexer); + case KeywordEnum::INPUT: + return $this->parseInputObjectTypeDefinition($lexer); + case KeywordEnum::EXTEND: + return $this->parseTypeExtension($lexer); + case KeywordEnum::DIRECTIVE: + return $this->parseDirectiveDefinition($lexer); + } + } - if (isOperation($value)) { - return $value; + throw $this->unexpected($lexer, $keywordToken); + } + + /** + * @param LexerInterface $lexer + * @return bool + */ + protected function peekDescription(LexerInterface $lexer): bool + { + return $this->peek($lexer, TokenKindEnum::STRING) || $this->peek($lexer, TokenKindEnum::BLOCK_STRING); + } + + /** + * @param LexerInterface $lexer + * @return array|null + * @throws GraphQLError + */ + protected function parseDescription(LexerInterface $lexer): ?array + { + return $this->peekDescription($lexer) ? $this->parseStringLiteral($lexer) : null; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseSchemaDefinition(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $this->expectKeyword($lexer, KeywordEnum::SCHEMA); + + $directives = $this->parseDirectives($lexer, true); + + $operationTypes = $this->many( + $lexer, + TokenKindEnum::BRACE_L, + [$this, 'parseOperationTypeDefinition'], + TokenKindEnum::BRACE_R + ); + + return [ + 'kind' => NodeKindEnum::SCHEMA_DEFINITION, + 'directives' => $directives, + 'operationTypes' => $operationTypes, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseOperationTypeDefinition(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $operation = $this->parseOperationType($lexer); + + $this->expect($lexer, TokenKindEnum::COLON); + + $type = $this->parseNamedType($lexer); + + return [ + 'kind' => NodeKindEnum::OPERATION_TYPE_DEFINITION, + 'operation' => $operation, + 'type' => $type, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseScalarTypeDefinition(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $description = $this->parseDescription($lexer); + + $this->expectKeyword($lexer, KeywordEnum::SCALAR); + + $name = $this->parseName($lexer); + $directives = $this->parseDirectives($lexer, true); + + return [ + 'kind' => NodeKindEnum::SCALAR_TYPE_DEFINITION, + 'description' => $description, + 'name' => $name, + 'directives' => $directives, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseObjectTypeDefinition(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $description = $this->parseDescription($lexer); + + $this->expectKeyword($lexer, KeywordEnum::TYPE); + + $name = $this->parseName($lexer); + $interfaces = $this->parseImplementsInterfaces($lexer); + $directives = $this->parseDirectives($lexer, true); + $fields = $this->parseFieldsDefinition($lexer); + + return [ + 'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION, + 'description' => $description, + 'name' => $name, + 'interfaces' => $interfaces, + 'directives' => $directives, + 'fields' => $fields, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseImplementsInterfaces(LexerInterface $lexer): array + { + $types = []; + + if ($lexer->getTokenValue() === 'implements') { + $lexer->advance(); + + // Optional leading ampersand + $this->skip($lexer, TokenKindEnum::AMP); + + do { + $types[] = $this->parseNamedType($lexer); + } while ($this->skip($lexer, TokenKindEnum::AMP)); } - throw $this->unexpected($lexer, $token); + return $types; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseFieldsDefinition(LexerInterface $lexer): array + { + return $this->peek($lexer, TokenKindEnum::BRACE_L) + ? $this->many($lexer, TokenKindEnum::BRACE_L, [$this, 'parseFieldDefinition'], TokenKindEnum::BRACE_R) + : []; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseFieldDefinition(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $description = $this->parseDescription($lexer); + $name = $this->parseName($lexer); + $arguments = $this->parseArgumentsDefinition($lexer); + + $this->expect($lexer, TokenKindEnum::COLON); + + $type = $this->parseTypeReference($lexer); + $directives = $this->parseDirectives($lexer, true); + + return [ + 'kind' => NodeKindEnum::FIELD_DEFINITION, + 'description' => $description, + 'name' => $name, + 'arguments' => $arguments, + 'type' => $type, + 'directives' => $directives, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseArgumentsDefinition(LexerInterface $lexer): array + { + return $this->peek($lexer, TokenKindEnum::PAREN_L) + ? $this->many($lexer, TokenKindEnum::PAREN_L, [$this, 'parseInputValueDefinition'], TokenKindEnum::PAREN_R) + : []; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseInputValueDefinition(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $description = $this->parseDescription($lexer); + $name = $this->parseName($lexer); + + $this->expect($lexer, TokenKindEnum::COLON); + + $type = $this->parseTypeReference($lexer); + $defaultValue = $this->skip($lexer, TokenKindEnum::EQUALS) ? $this->parseConstValue($lexer) : null; + $directives = $this->parseDirectives($lexer, true); + + return [ + 'kind' => NodeKindEnum::INPUT_VALUE_DEFINITION, + 'description' => $description, + 'name' => $name, + 'type' => $type, + 'defaultValue' => $defaultValue, + 'directives' => $directives, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseInterfaceTypeDefinition(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $description = $this->parseDescription($lexer); + + $this->expectKeyword($lexer, KeywordEnum::INTERFACE); + + $name = $this->parseName($lexer); + $directives = $this->parseDirectives($lexer, true); + $fields = $this->parseFieldsDefinition($lexer); + + return [ + 'kind' => NodeKindEnum::INTERFACE_TYPE_DEFINITION, + 'description' => $description, + 'name' => $name, + 'directives' => $directives, + 'fields' => $fields, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseUnionTypeDefinition(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $description = $this->parseDescription($lexer); + + $this->expectKeyword($lexer, KeywordEnum::UNION); + + $name = $this->parseName($lexer); + $directives = $this->parseDirectives($lexer, true); + $types = $this->parseUnionMemberTypes($lexer); + + return [ + 'kind' => NodeKindEnum::UNION_TYPE_DEFINITION, + 'description' => $description, + 'name' => $name, + 'directives' => $directives, + 'types' => $types, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseUnionMemberTypes(LexerInterface $lexer): array + { + $types = []; + + if ($this->skip($lexer, TokenKindEnum::EQUALS)) { + // Optional leading pipe + $this->skip($lexer, TokenKindEnum::PIPE); + + do { + $types[] = $this->parseNamedType($lexer); + } while ($this->skip($lexer, TokenKindEnum::PIPE)); + } + + return $types; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseEnumTypeDefinition(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $description = $this->parseDescription($lexer); + + $this->expectKeyword($lexer, KeywordEnum::ENUM); + + $name = $this->parseName($lexer); + $directives = $this->parseDirectives($lexer, true); + $values = $this->parseEnumValuesDefinition($lexer); + + return [ + 'kind' => NodeKindEnum::ENUM_TYPE_DEFINITION, + 'description' => $description, + 'name' => $name, + 'directives' => $directives, + 'values' => $values, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseEnumValuesDefinition(LexerInterface $lexer): array + { + return $this->peek($lexer, TokenKindEnum::BRACE_L) + ? $this->many($lexer, TokenKindEnum::BRACE_L, [$this, 'parseEnumValueDefinition'], TokenKindEnum::BRACE_R) + : []; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseEnumValueDefinition(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $description = $this->parseDescription($lexer); + $name = $this->parseName($lexer); + $directives = $this->parseDirectives($lexer, true); + + return [ + 'kind' => NodeKindEnum::ENUM_VALUE_DEFINITION, + 'description' => $description, + 'name' => $name, + 'directives' => $directives, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseInputObjectTypeDefinition(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $description = $this->parseDescription($lexer); + + $this->expectKeyword($lexer, KeywordEnum::INPUT); + + $name = $this->parseName($lexer); + $directives = $this->parseDirectives($lexer, true); + $fields = $this->parseInputFieldsDefinition($lexer); + + return [ + 'kind' => NodeKindEnum::INPUT_OBJECT_TYPE_DEFINITION, + 'description' => $description, + 'name' => $name, + 'directives' => $directives, + 'fields' => $fields, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseInputFieldsDefinition(LexerInterface $lexer): array + { + return $this->peek($lexer, TokenKindEnum::BRACE_L) + ? $this->many($lexer, TokenKindEnum::BRACE_L, [$this, 'parseInputValueDefinition'], TokenKindEnum::BRACE_R) + : []; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseTypeExtension(LexerInterface $lexer): array + { + $keywordToken = $lexer->lookahead(); + + if ($keywordToken->getKind() === TokenKindEnum::NAME) { + switch ($keywordToken->getValue()) { + case KeywordEnum::SCALAR: + return $this->parseScalarTypeExtension($lexer); + case KeywordEnum::TYPE: + return $this->parseObjectTypeExtension($lexer); + case KeywordEnum::INTERFACE: + return $this->parseInterfaceTypeExtension($lexer); + case KeywordEnum::UNION: + return $this->parseUnionTypeExtension($lexer); + case KeywordEnum::ENUM: + return $this->parseEnumTypeExtension($lexer); + case KeywordEnum::INPUT: + return $this->parseInputObjectTypeExtension($lexer); + } + } + + throw $this->unexpected($lexer, $keywordToken); + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseScalarTypeExtension(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $this->expectKeyword($lexer, KeywordEnum::EXTEND); + $this->expectKeyword($lexer, KeywordEnum::SCALAR); + + $name = $this->parseName($lexer); + $directives = $this->parseDirectives($lexer, true); + + if (count($directives) === 0) { + throw $this->unexpected($lexer); + } + + return [ + 'kind' => NodeKindEnum::SCALAR_TYPE_EXTENSION, + 'name' => $name, + 'directives' => $directives, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseObjectTypeExtension(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $this->expectKeyword($lexer, KeywordEnum::EXTEND); + $this->expectKeyword($lexer, KeywordEnum::TYPE); + + $name = $this->parseName($lexer); + $interfaces = $this->parseImplementsInterfaces($lexer); + $directives = $this->parseDirectives($lexer, true); + $fields = $this->parseFieldsDefinition($lexer); + + if (count($interfaces) === 0 && count($directives) === 0 && count($fields) === 0) { + throw $this->unexpected($lexer); + } + + return [ + 'kind' => NodeKindEnum::OBJECT_TYPE_EXTENSION, + 'name' => $name, + 'interfaces' => $interfaces, + 'directives' => $directives, + 'fields' => $fields, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseInterfaceTypeExtension(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $this->expectKeyword($lexer, KeywordEnum::EXTEND); + $this->expectKeyword($lexer, KeywordEnum::INTERFACE); + + $name = $this->parseName($lexer); + $directives = $this->parseDirectives($lexer, true); + $fields = $this->parseFieldsDefinition($lexer); + + if (count($directives) === 0 && count($fields) === 0) { + throw $this->unexpected($lexer); + } + + return [ + 'kind' => NodeKindEnum::INTERFACE_TYPE_EXTENSION, + 'name' => $name, + 'directives' => $directives, + 'fields' => $fields, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseUnionTypeExtension(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $this->expectKeyword($lexer, KeywordEnum::EXTEND); + $this->expectKeyword($lexer, KeywordEnum::UNION); + + $name = $this->parseName($lexer); + $directives = $this->parseDirectives($lexer, true); + $types = $this->parseUnionMemberTypes($lexer); + + if (count($directives) === 0 && count($types) === 0) { + throw $this->unexpected($lexer); + } + + return [ + 'kind' => NodeKindEnum::UNION_TYPE_EXTENSION, + 'name' => $name, + 'directives' => $directives, + 'types' => $types, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseEnumTypeExtension(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $this->expectKeyword($lexer, KeywordEnum::EXTEND); + $this->expectKeyword($lexer, KeywordEnum::ENUM); + + $name = $this->parseName($lexer); + $directives = $this->parseDirectives($lexer, true); + $values = $this->parseEnumValuesDefinition($lexer); + + if (count($directives) === 0 && count($values) === 0) { + throw $this->unexpected($lexer); + } + + return [ + 'kind' => NodeKindEnum::ENUM_TYPE_EXTENSION, + 'name' => $name, + 'directives' => $directives, + 'values' => $values, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + */ + protected function parseInputObjectTypeExtension(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $this->expectKeyword($lexer, KeywordEnum::EXTEND); + $this->expectKeyword($lexer, KeywordEnum::INPUT); + + $name = $this->parseName($lexer); + $directives = $this->parseDirectives($lexer, true); + $fields = $this->parseInputFieldsDefinition($lexer); + + if (count($directives) === 0 && count($fields) === 0) { + throw $this->unexpected($lexer); + } + + return [ + 'kind' => NodeKindEnum::INPUT_OBJECT_TYPE_EXTENSION, + 'name' => $name, + 'directives' => $directives, + 'fields' => $fields, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + * @throws \ReflectionException + */ + protected function parseDirectiveDefinition(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $description = $this->parseDescription($lexer); + + $this->expectKeyword($lexer, KeywordEnum::DIRECTIVE); + $this->expect($lexer, TokenKindEnum::AT); + + $name = $this->parseName($lexer); + $arguments = $this->parseArgumentsDefinition($lexer); + + $this->expectKeyword($lexer, KeywordEnum::ON); + + $locations = $this->parseDirectiveLocations($lexer); + + return [ + 'kind' => NodeKindEnum::DIRECTIVE_DEFINITION, + 'description' => $description, + 'name' => $name, + 'arguments' => $arguments, + 'locations' => $locations, + 'loc' => $this->createLocation($lexer, $start), + ]; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + * @throws \ReflectionException + */ + protected function parseDirectiveLocations(LexerInterface $lexer): array + { + $this->skip($lexer, TokenKindEnum::PIPE); + + $locations = []; + + do { + $locations[] = $this->parseDirectiveLocation($lexer); + } while ($this->skip($lexer, TokenKindEnum::PIPE)); + + return $locations; + } + + /** + * @param LexerInterface $lexer + * @return array + * @throws GraphQLError + * @throws \ReflectionException + */ + protected function parseDirectiveLocation(LexerInterface $lexer): array + { + $start = $lexer->getToken(); + + $name = $this->parseName($lexer); + + if (\in_array($name['value'], DirectiveLocationEnum::values(), true)) { + return $name; + } + + throw $this->unexpected($lexer, $start); } } diff --git a/src/Language/Contract/LexerInterface.php b/src/Language/Contract/LexerInterface.php new file mode 100644 index 00000000..dad866b7 --- /dev/null +++ b/src/Language/Contract/LexerInterface.php @@ -0,0 +1,58 @@ +getConstants()); + } +} diff --git a/src/Language/Lexer.php b/src/Language/Lexer.php index cbf584de..b480fa94 100644 --- a/src/Language/Lexer.php +++ b/src/Language/Lexer.php @@ -4,9 +4,10 @@ use Digia\GraphQL\Error\GraphQLError; use Digia\GraphQL\Error\SyntaxError; +use Digia\GraphQL\Language\Contract\LexerInterface; use Digia\GraphQL\Language\Reader\Contract\ReaderInterface; -class Lexer +class Lexer implements LexerInterface { /** @@ -77,43 +78,59 @@ public function __construct(Source $source, array $readers, array $options = []) } /** - * Advances the token stream to the next non-ignored token. - * - * @return Token - * @throws GraphQLError + * @inheritdoc */ public function advance(): Token { $this->lastToken = $this->token; - return $this->token = $this->lookAhead(); + + return $this->token = $this->lookahead(); } /** - * Looks ahead and returns the next non-ignored token, but does not change - * the Lexer's state. - * - * @return Token - * @throws GraphQLError + * @inheritdoc */ - public function lookAhead(): Token + public function lookahead(): Token { $token = $this->token; - if ($token->getKind() !== TokenKindEnum::EOF) { + + if (TokenKindEnum::EOF !== $token->getKind()) { do { - $next = $token->getNext(); - if ($next === null) { - $next = $this->readToken($token); - $token->setNext($next); - } + $next = $this->readToken($token); + $token->setNext($next); $token = $next; - } while ($token->getKind() === TokenKindEnum::COMMENT); - $this->token = $token; + } while (TokenKindEnum::COMMENT === $token->getKind()); } + return $token; } /** - * @return Token + * @inheritdoc + */ + public function getBody(): string + { + return $this->source->getBody(); + } + + /** + * @inheritdoc + */ + public function getTokenKind(): string + { + return $this->token->getKind(); + } + + /** + * @inheritdoc + */ + public function getTokenValue(): ?string + { + return $this->token->getValue(); + } + + /** + * @inheritdoc */ public function getToken(): Token { @@ -121,7 +138,7 @@ public function getToken(): Token } /** - * @return Source + * @inheritdoc */ public function getSource(): Source { @@ -129,7 +146,7 @@ public function getSource(): Source } /** - * @return Token + * @inheritdoc */ public function getLastToken(): Token { @@ -137,11 +154,21 @@ public function getLastToken(): Token } /** - * @return string + * @param int $code + * @param int $pos + * @param int $line + * @param int $col + * @param Token $prev + * @return Token + * @throws SyntaxError */ - public function getBody(): string + public function read(int $code, int $pos, int $line, int $col, Token $prev): Token { - return $this->source->getBody(); + if (($reader = $this->getReader($code, $pos)) !== null) { + return $reader->read($code, $pos, $line, $col, $prev); + } + + throw new SyntaxError($this->unexpectedCharacterMessage($code)); } /** @@ -171,24 +198,6 @@ protected function readToken(Token $prev): Token return $this->read($code, $pos, $line, $col, $prev); } - /** - * @param int $code - * @param int $pos - * @param int $line - * @param int $col - * @param Token $prev - * @return Token - * @throws SyntaxError - */ - public function read(int $code, int $pos, int $line, int $col, Token $prev): Token - { - if (($reader = $this->getReader($code, $pos)) !== null) { - return $reader->read($code, $pos, $line, $col, $prev); - } - - throw new SyntaxError($this->unexpectedCharacterMessage($code)); - } - /** * @param int $code * @return string diff --git a/src/Language/Token.php b/src/Language/Token.php index d1f4061a..f719301a 100644 --- a/src/Language/Token.php +++ b/src/Language/Token.php @@ -2,7 +2,10 @@ namespace Digia\GraphQL\Language; -class Token +use Digia\GraphQL\Contract\SerializationInterface; +use function Digia\GraphQL\Util\jsonEncode; + +class Token implements SerializationInterface { /** @@ -149,16 +152,24 @@ public function getValue() } /** - * @return string + * @inheritdoc */ - public function toJSON(): string + public function toArray(): array { - return json_encode([ + return [ 'kind' => $this->kind, 'value' => $this->value, 'line' => $this->line, 'column' => $this->column, - ]); + ]; + } + + /** + * @return string + */ + public function toJSON(): string + { + return jsonEncode($this->toArray()); } /** diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index 2b3e41d3..e4cd8823 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -3,12 +3,10 @@ namespace Digia\GraphQL\Type\Definition; use Digia\GraphQL\ConfigObject; -use Digia\GraphQL\Contract\SerializationInterface; use Digia\GraphQL\Type\Definition\Behavior\ArgumentsTrait; use Digia\GraphQL\Type\Definition\Behavior\DescriptionTrait; use Digia\GraphQL\Type\Definition\Behavior\NameTrait; use Digia\GraphQL\Type\Definition\Contract\DirectiveInterface; -use function Digia\GraphQL\Util\jsonEncode; class Directive extends ConfigObject implements DirectiveInterface { diff --git a/tests/Functional/Language/AbstractParserTest.php b/tests/Functional/Language/AbstractParserTest.php index f0a7e484..9ccca382 100644 --- a/tests/Functional/Language/AbstractParserTest.php +++ b/tests/Functional/Language/AbstractParserTest.php @@ -8,6 +8,7 @@ use Digia\GraphQL\Language\AST\Builder\DocumentBuilder; use Digia\GraphQL\Language\AST\Builder\EnumBuilder; use Digia\GraphQL\Language\AST\Builder\FieldBuilder; +use Digia\GraphQL\Language\AST\Builder\FieldDefinitionBuilder; use Digia\GraphQL\Language\AST\Builder\FloatBuilder; use Digia\GraphQL\Language\AST\Builder\FragmentDefinitionBuilder; use Digia\GraphQL\Language\AST\Builder\FragmentSpreadBuilder; @@ -21,6 +22,7 @@ use Digia\GraphQL\Language\AST\Builder\NullBuilder; use Digia\GraphQL\Language\AST\Builder\ObjectBuilder; use Digia\GraphQL\Language\AST\Builder\ObjectFieldBuilder; +use Digia\GraphQL\Language\AST\Builder\ObjectTypeDefinitionBuilder; use Digia\GraphQL\Language\AST\Builder\OperationDefinitionBuilder; use Digia\GraphQL\Language\AST\Builder\SelectionSetBuilder; use Digia\GraphQL\Language\AST\Builder\StringBuilder; @@ -56,6 +58,7 @@ abstract class AbstractParserTest extends TestCase public function setUp() { $builders = [ + // Standard new ArgumentBuilder(), new BooleanBuilder(), new DirectiveBuilder(), @@ -80,6 +83,9 @@ public function setUp() new StringBuilder(), new VariableBuilder(), new VariableDefinitionBuilder(), + // Experimental + new FieldDefinitionBuilder(), + new ObjectTypeDefinitionBuilder(), ]; $readers = [ diff --git a/tests/Functional/Language/SchemaParserTest.php b/tests/Functional/Language/SchemaParserTest.php index 882b9a21..1e321b21 100644 --- a/tests/Functional/Language/SchemaParserTest.php +++ b/tests/Functional/Language/SchemaParserTest.php @@ -1,14 +1,135 @@ NodeKindEnum::NAMED_TYPE, + 'name' => nameNode($name, $loc), + 'loc' => $loc, + ]; +} + +function nameNode($name, $loc) +{ + return [ + 'kind' => NodeKindEnum::NAME, + 'value' => $name, + 'loc' => $loc, + ]; +} + +function fieldNode($name, $type, $loc) +{ + return fieldNodeWithArgs($name, $type, [], $loc); +} + +function fieldNodeWithArgs($name, $type, $arguments, $loc) +{ + return [ + 'kind' => NodeKindEnum::FIELD_DEFINITION, + 'description' => null, + 'name' => $name, + 'arguments' => $arguments, + 'type' => $type, + 'directives' => [], + 'loc' => $loc, + ]; +} + +function enumValueNode($name, $loc) +{ + return [ + 'kind' => NodeKindEnum::ENUM_VALUE_DEFINITION, + 'name' => nameNode($name, $loc), + 'directives' => [], + 'loc' => $loc, + ]; +} + +function inputValueNode($name, $type, $defaultValue, $loc) { + return [ + 'kind' => NodeKindEnum::INPUT_VALUE_DEFINITION, + 'name' => $name, + 'type' => $type, + 'defaultValue' => $defaultValue, + 'directives' => [], + 'loc' => $loc, + ]; +} + +class SchemaParserTest extends AbstractParserTest +{ + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSimpleType() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source(' +type Hello { + world: String +}')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION, + 'description' => null, + 'name' => nameNode('Hello', ['start' => 6, 'end' => 11]), + 'interfaces' => [], + 'directives' => [], + 'fields' => [ + fieldNode( + nameNode('world', ['start' => 16, 'end' => 21]), + typeNode('String', ['start' => 23, 'end' => 29]), + ['start' => 16, 'end' => 29] + ), + ], + 'loc' => ['start' => 1, 'end' => 31], + ], + ], + 'loc' => ['start' => 0, 'end' => 31], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testParsesWithDescriptionString() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source(' +"Description" +type Hello { + world: String +}')); + $this->assertArraySubset([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION, + 'name' => nameNode('Hello', ['start' => 20, 'end' => 25]), + 'description' => [ + 'kind' => NodeKindEnum::STRING, + 'value' => 'Description', + 'loc' => ['start' => 1, 'end' => 14], + ], + ], + ], + 'loc' => ['start' => 0, 'end' => 45], + ], $node->toArray()); + } }