From 3ebe594df10cb580518885b5920e52f8bf1dfa2a Mon Sep 17 00:00:00 2001 From: Christoffer Niska Date: Sun, 25 Feb 2018 02:32:05 +0200 Subject: [PATCH 1/9] Add support for parsing schema definition --- src/Language/AST/Builder/AbstractBuilder.php | 2 +- src/Language/AST/Builder/BooleanBuilder.php | 2 +- src/Language/AST/Builder/EnumBuilder.php | 2 +- .../AST/Builder/EnumTypeDefinitionBuilder.php | 33 + .../Builder/EnumValueDefinitionBuilder.php | 32 + .../AST/Builder/FieldDefinitionBuilder.php | 34 + src/Language/AST/Builder/FloatBuilder.php | 2 +- .../Builder/InputValueDefinitionBuilder.php | 34 + src/Language/AST/Builder/IntBuilder.php | 2 +- .../InterfaceTypeDefinitionBuilder.php | 33 + src/Language/AST/Builder/NameBuilder.php | 2 +- .../Builder/ObjectTypeDefinitionBuilder.php | 34 + .../Builder/ObjectTypeExtensionBuilder.php | 33 + .../Builder/OperationDefinitionBuilder.php | 2 +- src/Language/AST/Builder/StringBuilder.php | 4 +- .../Builder/UnionTypeDefinitionBuilder.php | 33 + src/Language/AST/DirectiveLocationEnum.php | 9 + .../AST/Node/Behavior/DefaultValueTrait.php | 15 +- .../AST/Node/Behavior/DescriptionTrait.php | 12 +- .../{ValuesTrait.php => EnumValuesTrait.php} | 13 +- .../AST/Node/Behavior/FieldsTrait.php | 11 + .../AST/Node/Behavior/InterfacesTrait.php | 11 + src/Language/AST/Node/Behavior/TypesTrait.php | 11 + src/Language/AST/Node/BooleanValueNode.php | 2 +- src/Language/AST/Node/DocumentNode.php | 2 +- .../AST/Node/EnumTypeDefinitionNode.php | 19 +- .../AST/Node/EnumTypeExtensionNode.php | 4 +- .../AST/Node/EnumValueDefinitionNode.php | 14 + src/Language/AST/Node/FieldDefinitionNode.php | 23 +- .../AST/Node/InputValueDefinitionNode.php | 18 +- .../AST/Node/InterfaceTypeDefinitionNode.php | 17 +- src/Language/AST/Node/ListTypeNode.php | 2 +- src/Language/AST/Node/NameNode.php | 2 +- src/Language/AST/Node/NamedTypeNode.php | 2 +- src/Language/AST/Node/NonNullTypeNode.php | 2 +- .../AST/Node/ObjectTypeDefinitionNode.php | 18 +- .../AST/Node/ObjectTypeExtensionNode.php | 15 + .../AST/Node/UnionTypeDefinitionNode.php | 21 +- src/Language/ASTParser.php | 949 +++++++++++++++--- src/Language/Contract/LexerInterface.php | 58 ++ src/Language/KeywordEnum.php | 33 + src/Language/Lexer.php | 97 +- src/Language/Token.php | 21 +- src/Type/Definition/Directive.php | 2 - src/Type/Definition/EnumType.php | 2 +- src/Type/Definition/InterfaceType.php | 2 +- .../{TypeEnum.php => TypeNameEnum.php} | 2 +- src/Type/Definition/UnionType.php | 2 +- src/Type/scalars.php | 12 +- .../Language/AbstractParserTest.php | 19 + .../Functional/Language/SchemaParserTest.php | 819 ++++++++++++++- tests/Functional/Type/DefinitionTest.php | 6 +- 52 files changed, 2325 insertions(+), 226 deletions(-) create mode 100644 src/Language/AST/Builder/EnumTypeDefinitionBuilder.php create mode 100644 src/Language/AST/Builder/EnumValueDefinitionBuilder.php create mode 100644 src/Language/AST/Builder/FieldDefinitionBuilder.php create mode 100644 src/Language/AST/Builder/InputValueDefinitionBuilder.php create mode 100644 src/Language/AST/Builder/InterfaceTypeDefinitionBuilder.php create mode 100644 src/Language/AST/Builder/ObjectTypeDefinitionBuilder.php create mode 100644 src/Language/AST/Builder/ObjectTypeExtensionBuilder.php create mode 100644 src/Language/AST/Builder/UnionTypeDefinitionBuilder.php rename src/Language/AST/Node/Behavior/{ValuesTrait.php => EnumValuesTrait.php} (53%) create mode 100644 src/Language/Contract/LexerInterface.php create mode 100644 src/Language/KeywordEnum.php rename src/Type/Definition/{TypeEnum.php => TypeNameEnum.php} (96%) 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/EnumTypeDefinitionBuilder.php b/src/Language/AST/Builder/EnumTypeDefinitionBuilder.php new file mode 100644 index 00000000..02f42845 --- /dev/null +++ b/src/Language/AST/Builder/EnumTypeDefinitionBuilder.php @@ -0,0 +1,33 @@ + $this->buildOne($ast, 'description'), + 'name' => $this->buildOne($ast, 'name'), + 'directives' => $this->buildMany($ast, 'directives'), + 'values' => $this->buildMany($ast, 'values'), + 'location' => $this->createLocation($ast), + ]); + } + + /** + * @inheritdoc + */ + public function supportsKind(string $kind): bool + { + return $kind === NodeKindEnum::ENUM_TYPE_DEFINITION; + } +} diff --git a/src/Language/AST/Builder/EnumValueDefinitionBuilder.php b/src/Language/AST/Builder/EnumValueDefinitionBuilder.php new file mode 100644 index 00000000..17e71d3b --- /dev/null +++ b/src/Language/AST/Builder/EnumValueDefinitionBuilder.php @@ -0,0 +1,32 @@ + $this->buildOne($ast, 'description'), + 'name' => $this->buildOne($ast, 'name'), + 'directives' => $this->buildMany($ast, 'directives'), + 'location' => $this->createLocation($ast), + ]); + } + + /** + * @inheritdoc + */ + public function supportsKind(string $kind): bool + { + return $kind === NodeKindEnum::ENUM_VALUE_DEFINITION; + } +} 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/InputValueDefinitionBuilder.php b/src/Language/AST/Builder/InputValueDefinitionBuilder.php new file mode 100644 index 00000000..1f5c065b --- /dev/null +++ b/src/Language/AST/Builder/InputValueDefinitionBuilder.php @@ -0,0 +1,34 @@ + $this->buildOne($ast, 'description'), + 'name' => $this->buildOne($ast, 'name'), + 'type' => $this->buildOne($ast, 'type'), + 'defaultValue' => $this->buildOne($ast, 'defaultValue'), + 'directives' => $this->buildMany($ast, 'directives'), + 'location' => $this->createLocation($ast), + ]); + } + + /** + * @inheritdoc + */ + public function supportsKind(string $kind): bool + { + return $kind === NodeKindEnum::INPUT_VALUE_DEFINITION; + } +} 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/InterfaceTypeDefinitionBuilder.php b/src/Language/AST/Builder/InterfaceTypeDefinitionBuilder.php new file mode 100644 index 00000000..accebf47 --- /dev/null +++ b/src/Language/AST/Builder/InterfaceTypeDefinitionBuilder.php @@ -0,0 +1,33 @@ + $this->buildOne($ast, 'description'), + 'name' => $this->buildOne($ast, 'name'), + 'directives' => $this->buildMany($ast, 'directives'), + 'fields' => $this->buildMany($ast, 'fields'), + 'location' => $this->createLocation($ast), + ]); + } + + /** + * @inheritdoc + */ + public function supportsKind(string $kind): bool + { + return $kind === NodeKindEnum::INTERFACE_TYPE_DEFINITION; + } +} 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/ObjectTypeExtensionBuilder.php b/src/Language/AST/Builder/ObjectTypeExtensionBuilder.php new file mode 100644 index 00000000..ef365891 --- /dev/null +++ b/src/Language/AST/Builder/ObjectTypeExtensionBuilder.php @@ -0,0 +1,33 @@ + $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_EXTENSION; + } +} 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/Builder/UnionTypeDefinitionBuilder.php b/src/Language/AST/Builder/UnionTypeDefinitionBuilder.php new file mode 100644 index 00000000..dd9263c7 --- /dev/null +++ b/src/Language/AST/Builder/UnionTypeDefinitionBuilder.php @@ -0,0 +1,33 @@ + $this->buildOne($ast, 'description'), + 'name' => $this->buildOne($ast, 'name'), + 'directives' => $this->buildMany($ast, 'directives'), + 'types' => $this->buildMany($ast, 'types'), + 'location' => $this->createLocation($ast), + ]); + } + + /** + * @inheritdoc + */ + public function supportsKind(string $kind): bool + { + return $kind === NodeKindEnum::UNION_TYPE_DEFINITION; + } +} 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/DefaultValueTrait.php b/src/Language/AST/Node/Behavior/DefaultValueTrait.php index ad48449c..9a927e90 100644 --- a/src/Language/AST/Node/Behavior/DefaultValueTrait.php +++ b/src/Language/AST/Node/Behavior/DefaultValueTrait.php @@ -2,21 +2,30 @@ namespace Digia\GraphQL\Language\AST\Node\Behavior; +use Digia\GraphQL\Contract\SerializationInterface; use Digia\GraphQL\Language\AST\Node\Contract\ValueNodeInterface; trait DefaultValueTrait { /** - * @var ValueNodeInterface + * @var ValueNodeInterface|SerializationInterface|null */ protected $defaultValue; /** - * @return string + * @return ValueNodeInterface|SerializationInterface|null */ - public function getDefaultValue(): string + public function getDefaultValue(): ?ValueNodeInterface { return $this->defaultValue; } + + /** + * @return array + */ + public function getDefaultValueAsArray(): ?array + { + return null !== $this->defaultValue ? $this->defaultValue->toArray() : null; + } } 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/ValuesTrait.php b/src/Language/AST/Node/Behavior/EnumValuesTrait.php similarity index 53% rename from src/Language/AST/Node/Behavior/ValuesTrait.php rename to src/Language/AST/Node/Behavior/EnumValuesTrait.php index 7b7552a8..7d6d07f2 100644 --- a/src/Language/AST/Node/Behavior/ValuesTrait.php +++ b/src/Language/AST/Node/Behavior/EnumValuesTrait.php @@ -2,9 +2,10 @@ namespace Digia\GraphQL\Language\AST\Node\Behavior; +use Digia\GraphQL\Contract\SerializationInterface; use Digia\GraphQL\Language\AST\Node\EnumValueDefinitionNode; -trait ValuesTrait +trait EnumValuesTrait { /** @@ -19,4 +20,14 @@ public function getValues(): array { return $this->values; } + + /** + * @return array + */ + public function getValuesAsArray(): array + { + return array_map(function (SerializationInterface $node) { + return $node->toArray(); + }, $this->values); + } } 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..f62ac31d 100644 --- a/src/Language/AST/Node/Behavior/InterfacesTrait.php +++ b/src/Language/AST/Node/Behavior/InterfacesTrait.php @@ -2,6 +2,7 @@ namespace Digia\GraphQL\Language\AST\Node\Behavior; +use Digia\GraphQL\Contract\SerializationInterface; use Digia\GraphQL\Language\AST\Node\NamedTypeNode; trait InterfacesTrait @@ -19,4 +20,14 @@ public function getInterfaces(): array { return $this->interfaces; } + + /** + * @return array + */ + public function getInterfacesAsArray(): array + { + return array_map(function (SerializationInterface $node) { + return $node->toArray(); + }, $this->interfaces); + } } diff --git a/src/Language/AST/Node/Behavior/TypesTrait.php b/src/Language/AST/Node/Behavior/TypesTrait.php index 2992f1d5..46b230b0 100644 --- a/src/Language/AST/Node/Behavior/TypesTrait.php +++ b/src/Language/AST/Node/Behavior/TypesTrait.php @@ -2,6 +2,7 @@ namespace Digia\GraphQL\Language\AST\Node\Behavior; +use Digia\GraphQL\Contract\SerializationInterface; use Digia\GraphQL\Language\AST\Node\NamedTypeNode; trait TypesTrait @@ -19,4 +20,14 @@ public function getTypes(): array { return $this->types; } + + /** + * @return array + */ + public function getTypesAsArray(): array + { + return array_map(function (SerializationInterface $node) { + return $node->toArray(); + }, $this->types); + } } diff --git a/src/Language/AST/Node/BooleanValueNode.php b/src/Language/AST/Node/BooleanValueNode.php index 07dd56ba..cee7e26c 100644 --- a/src/Language/AST/Node/BooleanValueNode.php +++ b/src/Language/AST/Node/BooleanValueNode.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/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/EnumTypeDefinitionNode.php b/src/Language/AST/Node/EnumTypeDefinitionNode.php index ac7d39f3..97134a9b 100644 --- a/src/Language/AST/Node/EnumTypeDefinitionNode.php +++ b/src/Language/AST/Node/EnumTypeDefinitionNode.php @@ -6,7 +6,7 @@ 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\ValuesTrait; +use Digia\GraphQL\Language\AST\Node\Behavior\EnumValuesTrait; use Digia\GraphQL\Language\AST\Node\Contract\DefinitionNodeInterface; class EnumTypeDefinitionNode extends AbstractNode implements DefinitionNodeInterface @@ -15,10 +15,25 @@ class EnumTypeDefinitionNode extends AbstractNode implements DefinitionNodeInter use DescriptionTrait; use NameTrait; use DirectivesTrait; - use ValuesTrait; + use EnumValuesTrait; /** * @var string */ protected $kind = NodeKindEnum::ENUM_TYPE_DEFINITION; + + /** + * @inheritdoc + */ + public function toArray(): array + { + return [ + 'kind' => $this->kind, + 'description' => $this->getDescriptionAsArray(), + 'name' => $this->getNameAsArray(), + 'directives' => $this->getDirectivesAsArray(), + 'values' => $this->getValuesAsArray(), + 'loc' => $this->getLocationAsArray(), + ]; + } } diff --git a/src/Language/AST/Node/EnumTypeExtensionNode.php b/src/Language/AST/Node/EnumTypeExtensionNode.php index 96dcae56..3ab812eb 100644 --- a/src/Language/AST/Node/EnumTypeExtensionNode.php +++ b/src/Language/AST/Node/EnumTypeExtensionNode.php @@ -5,7 +5,7 @@ use Digia\GraphQL\Language\AST\NodeKindEnum; use Digia\GraphQL\Language\AST\Node\Behavior\DirectivesTrait; use Digia\GraphQL\Language\AST\Node\Behavior\NameTrait; -use Digia\GraphQL\Language\AST\Node\Behavior\ValuesTrait; +use Digia\GraphQL\Language\AST\Node\Behavior\EnumValuesTrait; use Digia\GraphQL\Language\AST\Node\Contract\TypeExtensionNodeInterface; class EnumTypeExtensionNode extends AbstractNode implements TypeExtensionNodeInterface @@ -13,7 +13,7 @@ class EnumTypeExtensionNode extends AbstractNode implements TypeExtensionNodeInt use NameTrait; use DirectivesTrait; - use ValuesTrait; + use EnumValuesTrait; /** * @var string diff --git a/src/Language/AST/Node/EnumValueDefinitionNode.php b/src/Language/AST/Node/EnumValueDefinitionNode.php index 7bc3c932..0280b050 100644 --- a/src/Language/AST/Node/EnumValueDefinitionNode.php +++ b/src/Language/AST/Node/EnumValueDefinitionNode.php @@ -19,4 +19,18 @@ class EnumValueDefinitionNode extends AbstractNode implements DefinitionNodeInte * @var string */ protected $kind = NodeKindEnum::ENUM_VALUE_DEFINITION; + + /** + * @inheritdoc + */ + public function toArray(): array + { + return [ + 'kind' => $this->kind, + 'description' => $this->getDescriptionAsArray(), + 'name' => $this->getNameAsArray(), + 'directives' => $this->getDirectivesAsArray(), + '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/InputValueDefinitionNode.php b/src/Language/AST/Node/InputValueDefinitionNode.php index 5f7b8191..3bb988ec 100644 --- a/src/Language/AST/Node/InputValueDefinitionNode.php +++ b/src/Language/AST/Node/InputValueDefinitionNode.php @@ -2,13 +2,13 @@ namespace Digia\GraphQL\Language\AST\Node; -use Digia\GraphQL\Language\AST\NodeKindEnum; use Digia\GraphQL\Language\AST\Node\Behavior\DefaultValueTrait; 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 InputValueDefinitionNode extends AbstractNode implements DefinitionNodeInterface { @@ -23,4 +23,20 @@ class InputValueDefinitionNode extends AbstractNode implements DefinitionNodeInt * @var string */ protected $kind = NodeKindEnum::INPUT_VALUE_DEFINITION; + + /** + * @inheritdoc + */ + public function toArray(): array + { + return [ + 'kind' => $this->kind, + 'description' => $this->getDescriptionAsArray(), + 'name' => $this->getNameAsArray(), + 'type' => $this->getTypeAsArray(), + 'defaultValue' => $this->getDefaultValueAsArray(), + 'directives' => $this->getDirectivesAsArray(), + 'loc' => $this->getLocationAsArray(), + ]; + } } diff --git a/src/Language/AST/Node/InterfaceTypeDefinitionNode.php b/src/Language/AST/Node/InterfaceTypeDefinitionNode.php index 43b7631e..67af8553 100644 --- a/src/Language/AST/Node/InterfaceTypeDefinitionNode.php +++ b/src/Language/AST/Node/InterfaceTypeDefinitionNode.php @@ -2,12 +2,12 @@ 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\NameTrait; use Digia\GraphQL\Language\AST\Node\Contract\DefinitionNodeInterface; +use Digia\GraphQL\Language\AST\NodeKindEnum; class InterfaceTypeDefinitionNode extends AbstractNode implements DefinitionNodeInterface { @@ -21,4 +21,19 @@ class InterfaceTypeDefinitionNode extends AbstractNode implements DefinitionNode * @var string */ protected $kind = NodeKindEnum::INTERFACE_TYPE_DEFINITION; + + /** + * @inheritdoc + */ + public function toArray(): array + { + return [ + 'kind' => $this->kind, + 'description' => $this->getDescriptionAsArray(), + 'name' => $this->getNameAsArray(), + 'directives' => $this->getDirectivesAsArray(), + 'fields' => $this->getFieldsAsArray(), + 'loc' => $this->getLocationAsArray(), + ]; + } } diff --git a/src/Language/AST/Node/ListTypeNode.php b/src/Language/AST/Node/ListTypeNode.php index 1daf3f4b..a4539be8 100644 --- a/src/Language/AST/Node/ListTypeNode.php +++ b/src/Language/AST/Node/ListTypeNode.php @@ -23,8 +23,8 @@ public function toArray(): array { return [ 'kind' => $this->kind, - 'loc' => $this->getLocationAsArray(), 'type' => $this->getTypeAsArray(), + '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/NonNullTypeNode.php b/src/Language/AST/Node/NonNullTypeNode.php index 9453cc74..e01e903b 100644 --- a/src/Language/AST/Node/NonNullTypeNode.php +++ b/src/Language/AST/Node/NonNullTypeNode.php @@ -23,8 +23,8 @@ public function toArray(): array { return [ 'kind' => $this->kind, - 'loc' => $this->getLocationAsArray(), 'type' => $this->getTypeAsArray(), + 'loc' => $this->getLocationAsArray(), ]; } } diff --git a/src/Language/AST/Node/ObjectTypeDefinitionNode.php b/src/Language/AST/Node/ObjectTypeDefinitionNode.php index 8a78e98f..c0c608f7 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,20 @@ 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/AST/Node/ObjectTypeExtensionNode.php b/src/Language/AST/Node/ObjectTypeExtensionNode.php index 4fd104e5..d1f6b098 100644 --- a/src/Language/AST/Node/ObjectTypeExtensionNode.php +++ b/src/Language/AST/Node/ObjectTypeExtensionNode.php @@ -21,4 +21,19 @@ class ObjectTypeExtensionNode extends AbstractNode implements TypeExtensionNodeI * @var string */ protected $kind = NodeKindEnum::OBJECT_TYPE_EXTENSION; + + /** + * @inheritdoc + */ + public function toArray(): array + { + return [ + 'kind' => $this->kind, + 'name' => $this->getNameAsArray(), + 'interfaces' => $this->getInterfacesAsArray(), + 'directives' => $this->getDirectivesAsArray(), + 'fields' => $this->getFieldsAsArray(), + 'loc' => $this->getLocationAsArray(), + ]; + } } diff --git a/src/Language/AST/Node/UnionTypeDefinitionNode.php b/src/Language/AST/Node/UnionTypeDefinitionNode.php index c5f1264a..17e70b7d 100644 --- a/src/Language/AST/Node/UnionTypeDefinitionNode.php +++ b/src/Language/AST/Node/UnionTypeDefinitionNode.php @@ -2,21 +2,38 @@ 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\NameTrait; use Digia\GraphQL\Language\AST\Node\Behavior\TypesTrait; use Digia\GraphQL\Language\AST\Node\Contract\DefinitionNodeInterface; +use Digia\GraphQL\Language\AST\NodeKindEnum; class UnionTypeDefinitionNode extends AbstractNode implements DefinitionNodeInterface { - use NameTrait; use DescriptionTrait; + use NameTrait; + use DirectivesTrait; use TypesTrait; /** * @var string */ protected $kind = NodeKindEnum::UNION_TYPE_DEFINITION; + + /** + * @inheritdoc + */ + public function toArray(): array + { + return [ + 'kind' => $this->kind, + 'description' => $this->getDescriptionAsArray(), + 'name' => $this->getNameAsArray(), + 'directives' => $this->getDirectivesAsArray(), + 'types' => $this->getTypesAsArray(), + '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/src/Type/Definition/EnumType.php b/src/Type/Definition/EnumType.php index 28e50c66..4bce924d 100644 --- a/src/Type/Definition/EnumType.php +++ b/src/Type/Definition/EnumType.php @@ -68,7 +68,7 @@ class EnumType extends ConfigObject implements TypeInterface, InputTypeInterface */ protected function beforeConfig(): void { - $this->setName(TypeEnum::ENUM); + $this->setName(TypeNameEnum::ENUM); } /** diff --git a/src/Type/Definition/InterfaceType.php b/src/Type/Definition/InterfaceType.php index 57e0944d..dba3730c 100644 --- a/src/Type/Definition/InterfaceType.php +++ b/src/Type/Definition/InterfaceType.php @@ -51,6 +51,6 @@ class InterfaceType extends ConfigObject implements AbstractTypeInterface, Compo */ protected function beforeConfig(): void { - $this->setName(TypeEnum::INTERFACE); + $this->setName(TypeNameEnum::INTERFACE); } } diff --git a/src/Type/Definition/TypeEnum.php b/src/Type/Definition/TypeNameEnum.php similarity index 96% rename from src/Type/Definition/TypeEnum.php rename to src/Type/Definition/TypeNameEnum.php index b3a9ae3c..43d71dda 100644 --- a/src/Type/Definition/TypeEnum.php +++ b/src/Type/Definition/TypeNameEnum.php @@ -2,7 +2,7 @@ namespace Digia\GraphQL\Type\Definition; -class TypeEnum +class TypeNameEnum { const INT = 'Int'; diff --git a/src/Type/Definition/UnionType.php b/src/Type/Definition/UnionType.php index 40c845b3..7dd1af86 100644 --- a/src/Type/Definition/UnionType.php +++ b/src/Type/Definition/UnionType.php @@ -68,7 +68,7 @@ class UnionType extends ConfigObject implements AbstractTypeInterface, Composite */ protected function beforeConfig(): void { - $this->setName(TypeEnum::UNION); + $this->setName(TypeNameEnum::UNION); } /** diff --git a/src/Type/scalars.php b/src/Type/scalars.php index 4879952b..c3eb093f 100644 --- a/src/Type/scalars.php +++ b/src/Type/scalars.php @@ -6,7 +6,7 @@ use Digia\GraphQL\Language\AST\Node\Contract\NodeInterface; use Digia\GraphQL\Type\Definition\Contract\TypeInterface; use Digia\GraphQL\Type\Definition\ScalarType; -use Digia\GraphQL\Type\Definition\TypeEnum; +use Digia\GraphQL\Type\Definition\TypeNameEnum; use function Digia\GraphQL\Util\arraySome; const MAX_INT = 2147483647; @@ -35,7 +35,7 @@ function GraphQLBoolean(): ScalarType if (!$instance) { $instance = GraphQLScalarType([ - 'name' => TypeEnum::BOOLEAN, + 'name' => TypeNameEnum::BOOLEAN, 'description' => 'The `Boolean` scalar type represents `true` or `false`.', 'serialize' => function ($value) { return coerceBoolean($value); @@ -79,7 +79,7 @@ function GraphQLFloat(): ScalarType if (!$instance) { $instance = GraphQLScalarType([ - 'name' => TypeEnum::FLOAT, + 'name' => TypeNameEnum::FLOAT, 'description' => 'The `Float` scalar type represents signed double-precision fractional ' . 'values as specified by ' . @@ -137,7 +137,7 @@ function GraphQLInt(): ScalarType if (!$instance) { $instance = GraphQLScalarType([ - 'name' => TypeEnum::INT, + 'name' => TypeNameEnum::INT, 'description' => 'The `Int` scalar type represents non-fractional signed whole numeric ' . 'values. Int can represent values between -(2^31) and 2^31 - 1.', @@ -191,7 +191,7 @@ function GraphQLID(): ScalarType if (!$instance) { $instance = GraphQLScalarType([ - 'name' => TypeEnum::ID, + 'name' => TypeNameEnum::ID, 'description' => 'The `ID` scalar type represents a unique identifier, often used to ' . 'refetch an object or as key for a cache. The ID type appears in a JSON ' . @@ -222,7 +222,7 @@ function GraphQLString(): ScalarType if (!$instance) { $instance = GraphQLScalarType([ - 'name' => TypeEnum::STRING, + 'name' => TypeNameEnum::STRING, 'description' => 'The `String` scalar type represents textual data, represented as UTF-8 ' . 'character sequences. The String type is most often used by GraphQL to ' . diff --git a/tests/Functional/Language/AbstractParserTest.php b/tests/Functional/Language/AbstractParserTest.php index f0a7e484..f8813a0d 100644 --- a/tests/Functional/Language/AbstractParserTest.php +++ b/tests/Functional/Language/AbstractParserTest.php @@ -7,12 +7,17 @@ use Digia\GraphQL\Language\AST\Builder\DirectiveBuilder; use Digia\GraphQL\Language\AST\Builder\DocumentBuilder; use Digia\GraphQL\Language\AST\Builder\EnumBuilder; +use Digia\GraphQL\Language\AST\Builder\EnumTypeDefinitionBuilder; +use Digia\GraphQL\Language\AST\Builder\EnumValueDefinitionBuilder; 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; use Digia\GraphQL\Language\AST\Builder\InlineFragmentBuilder; +use Digia\GraphQL\Language\AST\Builder\InputValueDefinitionBuilder; use Digia\GraphQL\Language\AST\Builder\IntBuilder; +use Digia\GraphQL\Language\AST\Builder\InterfaceTypeDefinitionBuilder; use Digia\GraphQL\Language\AST\Builder\ListBuilder; use Digia\GraphQL\Language\AST\Builder\ListTypeBuilder; use Digia\GraphQL\Language\AST\Builder\NameBuilder; @@ -21,9 +26,12 @@ 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\ObjectTypeExtensionBuilder; use Digia\GraphQL\Language\AST\Builder\OperationDefinitionBuilder; use Digia\GraphQL\Language\AST\Builder\SelectionSetBuilder; use Digia\GraphQL\Language\AST\Builder\StringBuilder; +use Digia\GraphQL\Language\AST\Builder\UnionTypeDefinitionBuilder; use Digia\GraphQL\Language\AST\Builder\VariableBuilder; use Digia\GraphQL\Language\AST\Builder\VariableDefinitionBuilder; use Digia\GraphQL\Language\ASTParser; @@ -48,6 +56,7 @@ abstract class AbstractParserTest extends TestCase { + /** * @var ParserInterface */ @@ -56,6 +65,7 @@ abstract class AbstractParserTest extends TestCase public function setUp() { $builders = [ + // Standard new ArgumentBuilder(), new BooleanBuilder(), new DirectiveBuilder(), @@ -80,6 +90,15 @@ public function setUp() new StringBuilder(), new VariableBuilder(), new VariableDefinitionBuilder(), + // Experimental + new FieldDefinitionBuilder(), + new ObjectTypeDefinitionBuilder(), + new ObjectTypeExtensionBuilder(), + new EnumTypeDefinitionBuilder(), + new EnumValueDefinitionBuilder(), + new InterfaceTypeDefinitionBuilder(), + new InputValueDefinitionBuilder(), + new UnionTypeDefinitionBuilder(), ]; $readers = [ diff --git a/tests/Functional/Language/SchemaParserTest.php b/tests/Functional/Language/SchemaParserTest.php index 882b9a21..a0b78bd9 100644 --- a/tests/Functional/Language/SchemaParserTest.php +++ b/tests/Functional/Language/SchemaParserTest.php @@ -1,14 +1,819 @@ 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, + 'description' => null, + 'name' => nameNode($name, $loc), + 'directives' => [], + 'loc' => $loc, + ]; +} + +function inputValueNode($name, $type, $defaultValue, $loc) { + return [ + 'kind' => NodeKindEnum::INPUT_VALUE_DEFINITION, + 'description' => null, + '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(TypeNameEnum::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()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testParsesWithDescriptionMultiLineString() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source(' +""" +Description +""" +# Even with comments between them +type Hello { + world: String +}')); + + $this->assertArraySubset([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION, + 'name' => nameNode('Hello', ['start' => 60, 'end' => 65]), + 'description' => [ + 'kind' => NodeKindEnum::STRING, + 'value' => 'Description', + 'loc' => ['start' => 1, 'end' => 20], + ], + ], + ], + 'loc' => ['start' => 0, 'end' => 85], + ], $node->toArray()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSimpleExtension() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source(' +extend type Hello { + world: String +}')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::OBJECT_TYPE_EXTENSION, + 'name' => nameNode('Hello', ['start' => 13, 'end' => 18]), + 'interfaces' => [], + 'directives' => [], + 'fields' => [ + fieldNode( + nameNode('world', ['start' => 23, 'end' => 28]), + typeNode(TypeNameEnum::STRING, ['start' => 30, 'end' => 36]), + ['start' => 23, 'end' => 36] + ), + ], + 'loc' => ['start' => 1, 'end' => 38], + ], + ], + 'loc' => ['start' => 0, 'end' => 38], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testExtensionWithoutFields() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source('extend type Hello implements Greeting')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::OBJECT_TYPE_EXTENSION, + 'name' => nameNode('Hello', ['start' => 12, 'end' => 17]), + 'interfaces' => [ + typeNode('Greeting', ['start' => 29, 'end' => 37]), + ], + 'directives' => [], + 'fields' => [], + 'loc' => ['start' => 0, 'end' => 37], + ], + ], + 'loc' => ['start' => 0, 'end' => 37], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testExtensionWithoutFieldsFollowedByExtension() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source(' + extend type Hello implements Greeting + + extend type Hello implements SecondGreeting + ')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::OBJECT_TYPE_EXTENSION, + 'name' => nameNode('Hello', ['start' => 19, 'end' => 24]), + 'interfaces' => [ + typeNode('Greeting', ['start' => 36, 'end' => 44]), + ], + 'directives' => [], + 'fields' => [], + 'loc' => ['start' => 7, 'end' => 44], + ], + [ + 'kind' => NodeKindEnum::OBJECT_TYPE_EXTENSION, + 'name' => nameNode('Hello', ['start' => 64, 'end' => 69]), + 'interfaces' => [ + typeNode('SecondGreeting', ['start' => 81, 'end' => 95]), + ], + 'directives' => [], + 'fields' => [], + 'loc' => ['start' => 52, 'end' => 95], + ], + ], + 'loc' => ['start' => 0, 'end' => 100], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testExtensionWithoutAnythingThrows() + { + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unexpected '); + $this->parser->parse(new Source('extend type Hello')); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testExtensionsDoNotIncludeDescriptions() + { + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unexpected Name "extend"'); + $this->parser->parse(new Source(' +"Description" +extend type Hello { + world: String +}')); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unexpected String "Description"'); + $this->parser->parse(new Source(' +extend "Description" type Hello { + world: String +}')); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSimpleNonNullType() + { + /** @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]), + [ + 'kind' => NodeKindEnum::NON_NULL_TYPE, + 'type' => typeNode(TypeNameEnum::STRING, ['start' => 23, 'end' => 29]), + 'loc' => ['start' => 23, 'end' => 30], + ], + ['start' => 16, 'end' => 30] + ), + ], + 'loc' => ['start' => 1, 'end' => 32], + ], + ], + 'loc' => ['start' => 0, 'end' => 32], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSimpleTypeInheritingInterface() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source('type Hello implements World { field: String }')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION, + 'description' => null, + 'name' => nameNode('Hello', ['start' => 5, 'end' => 10]), + 'interfaces' => [ + typeNode('World', ['start' => 22, 'end' => 27]), + ], + 'directives' => [], + 'fields' => [ + fieldNode( + nameNode('field', ['start' => 30, 'end' => 35]), + typeNode(TypeNameEnum::STRING, ['start' => 37, 'end' => 43]), + ['start' => 30, 'end' => 43] + ), + ], + 'loc' => ['start' => 0, 'end' => 45], + ], + ], + 'loc' => ['start' => 0, 'end' => 45], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSimpleTypeInheritingMultipleInterface() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source('type Hello implements Wo & rld { field: String }')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION, + 'description' => null, + 'name' => nameNode('Hello', ['start' => 5, 'end' => 10]), + 'interfaces' => [ + typeNode('Wo', ['start' => 22, 'end' => 24]), + typeNode('rld', ['start' => 27, 'end' => 30]), + ], + 'directives' => [], + 'fields' => [ + fieldNode( + nameNode('field', ['start' => 33, 'end' => 38]), + typeNode(TypeNameEnum::STRING, ['start' => 40, 'end' => 46]), + ['start' => 33, 'end' => 46] + ), + ], + 'loc' => ['start' => 0, 'end' => 48], + ], + ], + 'loc' => ['start' => 0, 'end' => 48], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSimpleTypeInheritingMultipleInterfaceWithLeadingAmpersand() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source('type Hello implements & Wo & rld { field: String }')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION, + 'description' => null, + 'name' => nameNode('Hello', ['start' => 5, 'end' => 10]), + 'interfaces' => [ + typeNode('Wo', ['start' => 24, 'end' => 26]), + typeNode('rld', ['start' => 29, 'end' => 32]), + ], + 'directives' => [], + 'fields' => [ + fieldNode( + nameNode('field', ['start' => 35, 'end' => 40]), + typeNode(TypeNameEnum::STRING, ['start' => 42, 'end' => 48]), + ['start' => 35, 'end' => 48] + ), + ], + 'loc' => ['start' => 0, 'end' => 50], + ], + ], + 'loc' => ['start' => 0, 'end' => 50], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSingleValueEnum() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source('enum Hello { WORLD }')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::ENUM_TYPE_DEFINITION, + 'description' => null, + 'name' => nameNode('Hello', ['start' => 5, 'end' => 10]), + 'directives' => [], + 'values' => [ + enumValueNode('WORLD', ['start' => 13, 'end' => 18]), + ], + 'loc' => ['start' => 0, 'end' => 20], + ], + ], + 'loc' => ['start' => 0, 'end' => 20], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testDoubleValueEnum() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source('enum Hello { WO, RLD }')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::ENUM_TYPE_DEFINITION, + 'description' => null, + 'name' => nameNode('Hello', ['start' => 5, 'end' => 10]), + 'directives' => [], + 'values' => [ + enumValueNode('WO', ['start' => 13, 'end' => 15]), + enumValueNode('RLD', ['start' => 17, 'end' => 20]), + ], + 'loc' => ['start' => 0, 'end' => 22], + ], + ], + 'loc' => ['start' => 0, 'end' => 22], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSimpleInterface() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source(' +interface Hello { + world: String +}')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::INTERFACE_TYPE_DEFINITION, + 'description' => null, + 'name' => nameNode('Hello', ['start' => 11, 'end' => 16]), + 'directives' => [], + 'fields' => [ + fieldNode( + nameNode('world', ['start' => 21, 'end' => 26]), + typeNode(TypeNameEnum::STRING, ['start' => 28, 'end' => 34]), + ['start' => 21, 'end' => 34] + ), + ], + 'loc' => ['start' => 1, 'end' => 36], + ], + ], + 'loc' => ['start' => 0, 'end' => 36], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function parseSimpleFieldWithArgument() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source(' +type Hello { + world(flag: Boolean): 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' => [ + fieldNodeWithArgs( + nameNode('world', ['start' => 16, 'end' => 21]), + typeNode(TypeNameEnum::STRING, ['start' => 45, 'end' => 51]), + [ + inputValueNode( + nameNode('flag', ['start' => 22, 'end' => 26]), + typeNode(TypeNameEnum::BOOLEAN, ['start' => 28, 'end' => 35]), + null, + ['start' => 22, 'end' => 35] + ), + ], + ['start' => 16, 'end' => 44] + ), + ], + 'loc' => ['start' => 1, 'end' => 46], + ], + ], + 'loc' => ['start' => 0, 'end' => 46], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSimpleFieldWithArgumentWithDefaultValue() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source(' +type Hello { + world(flag: Boolean = true): 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' => [ + fieldNodeWithArgs( + nameNode('world', ['start' => 16, 'end' => 21]), + typeNode(TypeNameEnum::STRING, ['start' => 45, 'end' => 51]), + [ + inputValueNode( + nameNode('flag', ['start' => 22, 'end' => 26]), + typeNode(TypeNameEnum::BOOLEAN, ['start' => 28, 'end' => 35]), + [ + 'kind' => NodeKindEnum::BOOLEAN, + 'value' => true, + 'loc' => ['start' => 38, 'end' => 42], + ], + ['start' => 22, 'end' => 42] + ), + ], + ['start' => 16, 'end' => 51] + ), + ], + 'loc' => ['start' => 1, 'end' => 53], + ], + ], + 'loc' => ['start' => 0, 'end' => 53], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSimpleFieldWithListArgument() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source(' +type Hello { + world(things: [String]): 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' => [ + fieldNodeWithArgs( + nameNode('world', ['start' => 16, 'end' => 21]), + typeNode(TypeNameEnum::STRING, ['start' => 41, 'end' => 47]), + [ + inputValueNode( + nameNode('things', ['start' => 22, 'end' => 28]), + [ + 'kind' => NodeKindEnum::LIST_TYPE, + 'type' => typeNode(TypeNameEnum::STRING, ['start' => 31, 'end' => 37]), + 'loc' => ['start' => 30, 'end' => 38], + ], + null, + ['start' => 22, 'end' => 38] + ), + ], + ['start' => 16, 'end' => 47] + ), + ], + 'loc' => ['start' => 1, 'end' => 49], + ], + ], + 'loc' => ['start' => 0, 'end' => 49], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function parseSimpleFieldWithTwoArguments() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source(' +type Hello { + world(argOne: Boolean, argTwo: Int): 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' => [ + fieldNodeWithArgs( + nameNode('world', ['start' => 16, 'end' => 21]), + typeNode(TypeNameEnum::STRING, ['start' => 53, 'end' => 59]), + [ + inputValueNode( + nameNode('argOne', ['start' => 22, 'end' => 28]), + typeNode(TypeNameEnum::BOOLEAN, ['start' => 30, 'end' => 37]), + null, + ['start' => 22, 'end' => 37] + ), + inputValueNode( + nameNode('argTwo', ['start' => 39, 'end' => 45]), + typeNode(TypeNameEnum::INT, ['start' => 47, 'end' => 50]), + null, + ['start' => 39, 'end' => 50] + ), + ], + ['start' => 16, 'end' => 59] + ), + ], + 'loc' => ['start' => 1, 'end' => 61], + ], + ], + 'loc' => ['start' => 0, 'end' => 61], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSimpleUnion() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source('union Hello = World')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::UNION_TYPE_DEFINITION, + 'description' => null, + 'name' => nameNode('Hello', ['start' => 6, 'end' => 11]), + 'directives' => [], + 'types' => [ + typeNode('World', ['start' => 14, 'end' => 19]), + ], + 'loc' => ['start' => 0, 'end' => 19], + ], + ], + 'loc' => ['start' => 0, 'end' => 19], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSimpleUnionWithTypes() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source('union Hello = Wo | Rld')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::UNION_TYPE_DEFINITION, + 'description' => null, + 'name' => nameNode('Hello', ['start' => 6, 'end' => 11]), + 'directives' => [], + 'types' => [ + typeNode('Wo', ['start' => 14, 'end' => 16]), + typeNode('Rld', ['start' => 19, 'end' => 22]), + ], + 'loc' => ['start' => 0, 'end' => 22], + ], + ], + 'loc' => ['start' => 0, 'end' => 22], + ]), $node->toJSON()); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testSimpleUnionWithTypesAndLeadingPipe() + { + /** @var DocumentNode $node */ + $node = $this->parser->parse(new Source('union Hello = | Wo | Rld')); + + $this->assertEquals(jsonEncode([ + 'kind' => NodeKindEnum::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKindEnum::UNION_TYPE_DEFINITION, + 'description' => null, + 'name' => nameNode('Hello', ['start' => 6, 'end' => 11]), + 'directives' => [], + 'types' => [ + typeNode('Wo', ['start' => 16, 'end' => 18]), + typeNode('Rld', ['start' => 21, 'end' => 24]), + ], + 'loc' => ['start' => 0, 'end' => 24], + ], + ], + 'loc' => ['start' => 0, 'end' => 24], + ]), $node->toJSON()); + } + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testUnionFailsWithNoTypes() + { + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Expected Name, found '); + $this->parser->parse(new Source('union Hello = |')); + } } diff --git a/tests/Functional/Type/DefinitionTest.php b/tests/Functional/Type/DefinitionTest.php index 5264b6d1..5e10f50a 100644 --- a/tests/Functional/Type/DefinitionTest.php +++ b/tests/Functional/Type/DefinitionTest.php @@ -11,7 +11,7 @@ use Digia\GraphQL\Type\Definition\ListType; use Digia\GraphQL\Type\Definition\ObjectType; use Digia\GraphQL\Type\Definition\ScalarType; -use Digia\GraphQL\Type\Definition\TypeEnum; +use Digia\GraphQL\Type\Definition\TypeNameEnum; use Digia\GraphQL\Type\Definition\UnionType; use Digia\GraphQL\Type\Schema\Schema; @@ -422,12 +422,12 @@ public function testIncludesInterfacePossibleTypesInSchemaTypeMap() */ public function testStringifySimpleTypes() { - $this->assertEquals(TypeEnum::INT, (string)GraphQLInt()); + $this->assertEquals(TypeNameEnum::INT, (string)GraphQLInt()); $this->assertEquals('Article', (string)$this->blogArticle); $this->assertEquals('Interface', (string)GraphQLInterfaceType()); $this->assertEquals('Union', (string)GraphQLUnionType()); $this->assertEquals('Enum', (string)GraphQLEnumType()); - $this->assertEquals(TypeEnum::INT, (string)GraphQLInt()); + $this->assertEquals(TypeNameEnum::INT, (string)GraphQLInt()); $this->assertEquals('Int!', (string)GraphQLNonNull(GraphQLInt())); $this->assertEquals('[Int]!', (string)GraphQLNonNull(GraphQLList(GraphQLInt()))); $this->assertEquals('[Int!]', (string)GraphQLList(GraphQLNonNull(GraphQLInt()))); From 85b3b4024b786287455f83b12c7d1911118b69cb Mon Sep 17 00:00:00 2001 From: Hung Neo Date: Sun, 25 Feb 2018 13:38:50 +0200 Subject: [PATCH 2/9] Throw GraphQLError instead of generic Exception --- src/Util/helpers.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Util/helpers.php b/src/Util/helpers.php index c80da359..356b72c4 100644 --- a/src/Util/helpers.php +++ b/src/Util/helpers.php @@ -2,15 +2,17 @@ namespace Digia\GraphQL\Util; +use Digia\GraphQL\Error\GraphQLError; + /** * @param bool $condition * @param string $message - * @throws \Exception + * @throws GraphQLError */ function invariant(bool $condition, string $message) { if (!$condition) { - throw new \Exception($message); + throw new GraphQLError($message); } } From 1fe7c2c546308afcc0cd13cd65d8237b7ffd5372 Mon Sep 17 00:00:00 2001 From: Hung Neo Date: Sun, 25 Feb 2018 14:58:54 +0200 Subject: [PATCH 3/9] Move query logic from Execution to Query strategy class --- src/Execution/Execution.php | 96 ++---------- src/Execution/Strategies/AbstractStrategy.php | 146 ++++++++++++++++++ src/Execution/Strategies/MutationStrategy.php | 21 +++ src/Execution/Strategies/QueryStrategy.php | 31 ++++ .../Strategies/SubscriptionStrategy.php | 21 +++ .../AST/Node/Contract/ValueNodeInterface.php | 5 +- src/Type/Definition/Behavior/FieldsTrait.php | 2 + tests/Functional/Excecution/ExecutionTest.php | 6 +- 8 files changed, 242 insertions(+), 86 deletions(-) create mode 100644 src/Execution/Strategies/AbstractStrategy.php create mode 100644 src/Execution/Strategies/MutationStrategy.php create mode 100644 src/Execution/Strategies/QueryStrategy.php create mode 100644 src/Execution/Strategies/SubscriptionStrategy.php diff --git a/src/Execution/Execution.php b/src/Execution/Execution.php index ba9452c2..589d525a 100644 --- a/src/Execution/Execution.php +++ b/src/Execution/Execution.php @@ -3,6 +3,9 @@ namespace Digia\GraphQL\Execution; use Digia\GraphQL\Error\GraphQLError; +use Digia\GraphQL\Execution\Strategies\MutationStrategy; +use Digia\GraphQL\Execution\Strategies\QueryStrategy; +use Digia\GraphQL\Execution\Strategies\SubscriptionStrategy; use Digia\GraphQL\Language\AST\Node\DocumentNode; use Digia\GraphQL\Language\AST\Node\FieldNode; use Digia\GraphQL\Language\AST\Node\OperationDefinitionNode; @@ -152,93 +155,18 @@ private function executeOperation( $rootValue ): ExecutionResult { - //MUTATION - //SUBSCRIPTION - //QUERY - - //result = executionStrategy.execute(executionContext, parameters); - // type: query|mutation|suscription - $query = $context->getSchema()->getQuery(); - $fields = $this->collectFields($query, $operation->getSelectionSet(), [], []); - $path = []; - - if ($context->getOperation()->getName()->getValue() === 'query') { - $data = $this->executeFields($query, $rootValue, $path, $fields); - - return new ExecutionResult($data, []); - } - - return new ExecutionResult([], []); - } - - private function collectFields( - ObjectType $runtimeType, - SelectionSetNode $selectionSet, - $fields, - $visitedFragmentNames - ) - { - foreach ($selectionSet->getSelections() as $selection) { - switch ($selection->getKind()) { - case NodeKindEnum::FIELD: - /** @var FieldNode $selection */ - $name = $selection->getName()->getValue(); - $fields[$name][] = $selection; - break; - } + $operationName = $context->getOperation()->getName()->getValue(); + + if ($operationName === 'subscription') { + $strategy = new SubscriptionStrategy($context, $operation, $rootValue); + } elseif ($operationName === 'mutation'){ + $strategy = new MutationStrategy($context, $operation, $rootValue); + } else { + $strategy = new QueryStrategy($context, $operation, $rootValue); } - return $fields; + return $strategy->execute(); } - /** - * Implements the "Evaluating selection sets" section of the spec - * for "read" mode. - * @param ObjectType $parentType - * @param $source - * @param $path - * @param $fields - * - * @return array - */ - private function executeFields(ObjectType $parentType, $source, $path, $fields): array - { - $finalResults = []; - foreach ($fields as $responseName => $fieldNodes) { - $fieldPath = $path; - $fieldPath[] = $responseName; - - $result = $this->resolveField($parentType, $source, $fieldNodes, $fieldPath); - $finalResults[$responseName] = $result; - } - - return $finalResults; - } - - /** - * @param ObjectType $parentType - * @param $source - * @param $fieldNodes - * @param $path - * - * @return array|\Exception|mixed|null - */ - private function resolveField(ObjectType $parentType, $source, $fieldNodes, $path) - { - /** @var FieldNode $fieldNode */ - $fieldNode = $fieldNodes[0]; - - $field = $parentType->getFields()[$fieldNode->getName()->getValue()]; - - $inputValues = $fieldNode->getArguments() ?? []; - - $args = []; - - foreach($inputValues as $value) { - $args[] = $value->getDefaultValue(); - } - - return $field->resolve(...$args); - } } diff --git a/src/Execution/Strategies/AbstractStrategy.php b/src/Execution/Strategies/AbstractStrategy.php new file mode 100644 index 00000000..c33b5371 --- /dev/null +++ b/src/Execution/Strategies/AbstractStrategy.php @@ -0,0 +1,146 @@ +context = $context; + $this->operation = $operation; + $this->rootValue = $rootValue; + } + + abstract function execute(): ExecutionResult; + + /** + * @param ObjectType $runtimeType + * @param SelectionSetNode $selectionSet + * @param $fields + * @param $visitedFragmentNames + * @return mixed + */ + protected function collectFields( + ObjectType $runtimeType, + SelectionSetNode $selectionSet, + $fields, + $visitedFragmentNames + ) { + foreach ($selectionSet->getSelections() as $selection) { + switch ($selection->getKind()) { + case NodeKindEnum::FIELD: + /** @var FieldNode $selection */ + $name = $selection->getName()->getValue(); + $fields[$name][] = $selection; + break; + } + } + + return $fields; + } + + /** + * Implements the "Evaluating selection sets" section of the spec + * for "read" mode. + * @param ObjectType $parentType + * @param $source + * @param $path + * @param $fields + * + * @return array + * + * @throws GraphQLError|\Exception + */ + protected function executeFields( + ObjectType $parentType, + $source, + $path, + $fields): array + { + $finalResults = []; + foreach ($fields as $responseName => $fieldNodes) { + $fieldPath = $path; + $fieldPath[] = $responseName; + + $result = $this->resolveField($parentType, + $source, + $fieldNodes, + $fieldPath + ); + + $finalResults[$responseName] = $result; + } + + return $finalResults; + } + + /** + * @param ObjectType $parentType + * @param $source + * @param $fieldNodes + * @param $path + * + * @return mixed + * + * @throws GraphQLError|\Exception + */ + protected function resolveField( + ObjectType $parentType, + $source, + $fieldNodes, + $path) + { + /** @var FieldNode $fieldNode */ + $fieldNode = $fieldNodes[0]; + + $field = $parentType->getFields()[$fieldNode->getName()->getValue()]; + + $inputValues = $fieldNode->getArguments() ?? []; + + $args = []; + + foreach ($inputValues as $value) { + $args[] = $value->getDefaultValue()->getValue(); + } + + return $field->resolve(...$args); + } +} diff --git a/src/Execution/Strategies/MutationStrategy.php b/src/Execution/Strategies/MutationStrategy.php new file mode 100644 index 00000000..368b9ec5 --- /dev/null +++ b/src/Execution/Strategies/MutationStrategy.php @@ -0,0 +1,21 @@ +context->getSchema()->getQuery(); + $fields = $this->collectFields($query, $this->operation->getSelectionSet(), [], []); + $path = []; + + try { + $data = $this->executeFields($query, $this->rootValue, $path, $fields); + } catch (\Exception $ex) { + return new ExecutionResult([], [$ex]); + } + + return new ExecutionResult($data, []); + } +} diff --git a/src/Execution/Strategies/SubscriptionStrategy.php b/src/Execution/Strategies/SubscriptionStrategy.php new file mode 100644 index 00000000..a98b2e9d --- /dev/null +++ b/src/Execution/Strategies/SubscriptionStrategy.php @@ -0,0 +1,21 @@ + 'name' ]), 'type' => GraphQLString(), - 'defaultValue' => 'Han Solo' + 'defaultValue' => new StringValueNode([ + 'value' => 'Han Solo', + ]), ]) ] ]) From ec10f0f8b8fe741c16ea2491fbb79c6be6444b20 Mon Sep 17 00:00:00 2001 From: Hung Neo Date: Mon, 26 Feb 2018 01:35:02 +0200 Subject: [PATCH 4/9] Add test for mutation --- src/Execution/Execution.php | 24 +++--- tests/Functional/Excecution/ExecutionTest.php | 75 +++++++++++++++++++ 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/src/Execution/Execution.php b/src/Execution/Execution.php index 589d525a..18f3758d 100644 --- a/src/Execution/Execution.php +++ b/src/Execution/Execution.php @@ -7,11 +7,8 @@ use Digia\GraphQL\Execution\Strategies\QueryStrategy; use Digia\GraphQL\Execution\Strategies\SubscriptionStrategy; use Digia\GraphQL\Language\AST\Node\DocumentNode; -use Digia\GraphQL\Language\AST\Node\FieldNode; use Digia\GraphQL\Language\AST\Node\OperationDefinitionNode; -use Digia\GraphQL\Language\AST\Node\SelectionSetNode; use Digia\GraphQL\Language\AST\NodeKindEnum; -use Digia\GraphQL\Type\Definition\ObjectType; use Digia\GraphQL\Type\Schema\Schema; /** @@ -52,8 +49,7 @@ public static function execute( $variableValues = null, $operationName = null, callable $fieldResolver = null - ) - { + ) { try { $context = self::buildExecutionContext( $schema, @@ -96,12 +92,11 @@ private static function buildExecutionContext( $rawVariableValues, $operationName = null, callable $fieldResolver = null - ): ExecutionContext - { + ): ExecutionContext { //TODO: Validate raw variables, operation name etc. //TODO: Validate document definition - $errors = []; + $errors = []; $fragments = []; $operation = null; @@ -113,12 +108,14 @@ private static function buildExecutionContext( 'Must provide operation name if query contains multiple operations.' ); } - if (!$operationName || (!empty($definition->getName()) && $definition->getName()->getValue() === $operationName)) { + if (!$operationName || (!empty($definition->getName()) && $definition->getName() + ->getValue() === $operationName)) { $operation = $definition; } break; case NodeKindEnum::FRAGMENT_DEFINITION: - $fragments[$definition->getName()->getValue()] = $definition; + $fragments[$definition->getName() + ->getValue()] = $definition; break; default: throw new GraphQLError( @@ -153,13 +150,12 @@ private function executeOperation( ExecutionContext $context, OperationDefinitionNode $operation, $rootValue - ): ExecutionResult - { + ): ExecutionResult { $operationName = $context->getOperation()->getName()->getValue(); if ($operationName === 'subscription') { $strategy = new SubscriptionStrategy($context, $operation, $rootValue); - } elseif ($operationName === 'mutation'){ + } elseif ($operationName === 'mutation') { $strategy = new MutationStrategy($context, $operation, $rootValue); } else { $strategy = new QueryStrategy($context, $operation, $rootValue); @@ -167,6 +163,4 @@ private function executeOperation( return $strategy->execute(); } - - } diff --git a/tests/Functional/Excecution/ExecutionTest.php b/tests/Functional/Excecution/ExecutionTest.php index 5aea00f8..76ca931b 100644 --- a/tests/Functional/Excecution/ExecutionTest.php +++ b/tests/Functional/Excecution/ExecutionTest.php @@ -318,4 +318,79 @@ public function testExecuteQueryWithMultipleFields() $this->assertEquals($expected, $executionResult); } + + public function testSimpleMutation() + { + $schema = new Schema([ + 'mutation' => new ObjectType([ + 'name' => 'M', + 'fields' => [ + 'name' => GraphQLString(), + 'resolve' => function($name) { + return sprintf("%s was written to database", $name); + } + ] + ]) + ]); + + $documentNode = new DocumentNode([ + 'definitions' => [ + new OperationDefinitionNode([ + 'kind' => NodeKindEnum::OPERATION_DEFINITION, + 'name' => new NameNode([ + 'value' => 'mutation' + ]), + 'selectionSet' => new SelectionSetNode([ + 'selections' => [ + new FieldNode([ + 'name' => new NameNode([ + 'value' => 'M', + 'location' => new Location( + 15, + 20, + new Source('mutation M { name }', 'GraphQL', new SourceLocation()) + ) + ]), + 'arguments' => [ + new InputValueDefinitionNode([ + 'name' => new NameNode([ + 'value' => 'name' + ]), + 'type' => GraphQLString(), + 'defaultValue' => new StringValueNode([ + 'value' => 'Han Solo', + ]), + ]) + ] + ]) + ] + ]), + 'operation' => 'query', + 'directives' => [], + 'variableDefinitions' => [] + ]) + ], + ]); + + $rootValue = []; + $contextValue = ''; + $variableValues = []; + $operationName = 'M'; + $fieldResolver = null; + + /** @var ExecutionResult $executionResult */ + $executionResult = Execution::execute( + $schema, + $documentNode, + $rootValue, + $contextValue, + $variableValues, + $operationName, + $fieldResolver + ); + + $expected = new ExecutionResult([], []); + + $this->assertEquals($expected, $executionResult); + } } From 57fc63267e88e2df9398db2f160f22f51978ef71 Mon Sep 17 00:00:00 2001 From: Hung Neo Date: Mon, 26 Feb 2018 17:29:53 +0200 Subject: [PATCH 5/9] Add new testcase for mutation --- src/Execution/Execution.php | 9 +-- src/Execution/Strategies/MutationStrategy.php | 12 ++- .../Builder/OperationDefinitionBuilder.php | 1 + .../ExecutionTest.php | 80 +------------------ tests/Functional/Execution/MutationTest.php | 69 ++++++++++++++++ 5 files changed, 87 insertions(+), 84 deletions(-) rename tests/Functional/{Excecution => Execution}/ExecutionTest.php (80%) create mode 100644 tests/Functional/Execution/MutationTest.php diff --git a/src/Execution/Execution.php b/src/Execution/Execution.php index 18f3758d..13121dbb 100644 --- a/src/Execution/Execution.php +++ b/src/Execution/Execution.php @@ -108,14 +108,13 @@ private static function buildExecutionContext( 'Must provide operation name if query contains multiple operations.' ); } - if (!$operationName || (!empty($definition->getName()) && $definition->getName() - ->getValue() === $operationName)) { + + if (!$operationName || (!empty($definition->getName()) && $definition->getName()->getValue() === $operationName)) { $operation = $definition; } break; case NodeKindEnum::FRAGMENT_DEFINITION: - $fragments[$definition->getName() - ->getValue()] = $definition; + $fragments[$definition->getName()->getValue()] = $definition; break; default: throw new GraphQLError( @@ -151,7 +150,7 @@ private function executeOperation( OperationDefinitionNode $operation, $rootValue ): ExecutionResult { - $operationName = $context->getOperation()->getName()->getValue(); + $operationName = $context->getOperation()->getOperation(); if ($operationName === 'subscription') { $strategy = new SubscriptionStrategy($context, $operation, $rootValue); diff --git a/src/Execution/Strategies/MutationStrategy.php b/src/Execution/Strategies/MutationStrategy.php index 368b9ec5..07f0c99b 100644 --- a/src/Execution/Strategies/MutationStrategy.php +++ b/src/Execution/Strategies/MutationStrategy.php @@ -16,6 +16,16 @@ class MutationStrategy extends AbstractStrategy */ public function execute(): ExecutionResult { - return new ExecutionResult([], []); + $mutation = $this->context->getSchema()->getMutation(); + $fields = $this->collectFields($mutation, $this->operation->getSelectionSet(), [], []); + $path = []; + + try { + $data = $this->executeFields($mutation, $this->rootValue, $path, $fields); + } catch (\Exception $ex) { + return new ExecutionResult([], [$ex]); + } + + return new ExecutionResult($data, []); } } diff --git a/src/Language/AST/Builder/OperationDefinitionBuilder.php b/src/Language/AST/Builder/OperationDefinitionBuilder.php index 68304dd1..e340937a 100644 --- a/src/Language/AST/Builder/OperationDefinitionBuilder.php +++ b/src/Language/AST/Builder/OperationDefinitionBuilder.php @@ -16,6 +16,7 @@ public function build(array $ast): NodeInterface { return new OperationDefinitionNode([ 'operation' => $this->get($ast, 'operation'), + 'name' => $this->buildOne($ast, 'name'), 'variableDefinitions' => $this->buildMany($ast, 'variableDefinitions'), 'directives' => $this->buildMany($ast, 'directives'), 'selectionSet' => $this->buildOne($ast, 'selectionSet'), diff --git a/tests/Functional/Excecution/ExecutionTest.php b/tests/Functional/Execution/ExecutionTest.php similarity index 80% rename from tests/Functional/Excecution/ExecutionTest.php rename to tests/Functional/Execution/ExecutionTest.php index 76ca931b..e7144916 100644 --- a/tests/Functional/Excecution/ExecutionTest.php +++ b/tests/Functional/Execution/ExecutionTest.php @@ -1,6 +1,6 @@ assertEquals($expected, $executionResult); } - - public function testSimpleMutation() - { - $schema = new Schema([ - 'mutation' => new ObjectType([ - 'name' => 'M', - 'fields' => [ - 'name' => GraphQLString(), - 'resolve' => function($name) { - return sprintf("%s was written to database", $name); - } - ] - ]) - ]); - - $documentNode = new DocumentNode([ - 'definitions' => [ - new OperationDefinitionNode([ - 'kind' => NodeKindEnum::OPERATION_DEFINITION, - 'name' => new NameNode([ - 'value' => 'mutation' - ]), - 'selectionSet' => new SelectionSetNode([ - 'selections' => [ - new FieldNode([ - 'name' => new NameNode([ - 'value' => 'M', - 'location' => new Location( - 15, - 20, - new Source('mutation M { name }', 'GraphQL', new SourceLocation()) - ) - ]), - 'arguments' => [ - new InputValueDefinitionNode([ - 'name' => new NameNode([ - 'value' => 'name' - ]), - 'type' => GraphQLString(), - 'defaultValue' => new StringValueNode([ - 'value' => 'Han Solo', - ]), - ]) - ] - ]) - ] - ]), - 'operation' => 'query', - 'directives' => [], - 'variableDefinitions' => [] - ]) - ], - ]); - - $rootValue = []; - $contextValue = ''; - $variableValues = []; - $operationName = 'M'; - $fieldResolver = null; - - /** @var ExecutionResult $executionResult */ - $executionResult = Execution::execute( - $schema, - $documentNode, - $rootValue, - $contextValue, - $variableValues, - $operationName, - $fieldResolver - ); - - $expected = new ExecutionResult([], []); - - $this->assertEquals($expected, $executionResult); - } } diff --git a/tests/Functional/Execution/MutationTest.php b/tests/Functional/Execution/MutationTest.php new file mode 100644 index 00000000..15bc9cb0 --- /dev/null +++ b/tests/Functional/Execution/MutationTest.php @@ -0,0 +1,69 @@ + + new ObjectType([ + 'name' => 'greeting', + 'fields' => [ + ' message' => [ + 'type' => GraphQLString(), + 'resolve' => function ($name) { + return sprintf("Hello %s. Record was written to database", $name); + } + ] + ] + ]) + ]); + + /** @var DocumentNode $documentNode */ + $documentNode = $this->parser->parse(new Source(' + mutation M{ + greeting(name:"Han Solo") { + message + } + } + ')); + + $rootValue = []; + $contextValue = ''; + $variableValues = []; + $operationName = 'M'; + $fieldResolver = null; + + /** @var ExecutionResult $executionResult */ + $executionResult = Execution::execute( + $schema, + $documentNode, + $rootValue, + $contextValue, + $variableValues, + $operationName, + $fieldResolver + ); + + $expected = new ExecutionResult([ + 'message' => 'Hello Han Solo. Record was written to database' + ], []); + + $this->assertEquals($expected, $executionResult); + } +} From b49e9995fa180dce72942897573e69e7b726c179 Mon Sep 17 00:00:00 2001 From: Hung Neo Date: Tue, 27 Feb 2018 00:47:22 +0200 Subject: [PATCH 6/9] Complete first test case for mutation --- src/Execution/Strategies/AbstractStrategy.php | 10 +++++--- src/Execution/Strategies/MutationStrategy.php | 2 +- tests/Functional/Execution/MutationTest.php | 23 +++++++++++++++---- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Execution/Strategies/AbstractStrategy.php b/src/Execution/Strategies/AbstractStrategy.php index c33b5371..71542255 100644 --- a/src/Execution/Strategies/AbstractStrategy.php +++ b/src/Execution/Strategies/AbstractStrategy.php @@ -5,10 +5,12 @@ use Digia\GraphQL\Error\GraphQLError; use Digia\GraphQL\Execution\ExecutionContext; use Digia\GraphQL\Execution\ExecutionResult; +use Digia\GraphQL\Language\AST\Node\ArgumentNode; use Digia\GraphQL\Language\AST\Node\Contract\ValueNodeInterface; use Digia\GraphQL\Language\AST\Node\FieldNode; use Digia\GraphQL\Language\AST\Node\OperationDefinitionNode; use Digia\GraphQL\Language\AST\Node\SelectionSetNode; +use Digia\GraphQL\Language\AST\Node\StringValueNode; use Digia\GraphQL\Language\AST\NodeKindEnum; use Digia\GraphQL\Type\Definition\ObjectType; @@ -73,7 +75,6 @@ protected function collectFields( break; } } - return $fields; } @@ -138,9 +139,12 @@ protected function resolveField( $args = []; foreach ($inputValues as $value) { - $args[] = $value->getDefaultValue()->getValue(); + if ($value instanceof ArgumentNode) { + $args[] = $value->getValue()->getValue(); + } elseif ($value instanceof StringValueNode) { + $args[] = $value->getDefaultValue()->getValue(); + } } - return $field->resolve(...$args); } } diff --git a/src/Execution/Strategies/MutationStrategy.php b/src/Execution/Strategies/MutationStrategy.php index 07f0c99b..e4f07601 100644 --- a/src/Execution/Strategies/MutationStrategy.php +++ b/src/Execution/Strategies/MutationStrategy.php @@ -23,7 +23,7 @@ public function execute(): ExecutionResult try { $data = $this->executeFields($mutation, $this->rootValue, $path, $fields); } catch (\Exception $ex) { - return new ExecutionResult([], [$ex]); + return new ExecutionResult([], [$ex->getMessage()]); } return new ExecutionResult($data, []); diff --git a/tests/Functional/Execution/MutationTest.php b/tests/Functional/Execution/MutationTest.php index 15bc9cb0..ef1fb558 100644 --- a/tests/Functional/Execution/MutationTest.php +++ b/tests/Functional/Execution/MutationTest.php @@ -5,9 +5,11 @@ use Digia\GraphQL\Execution\Execution; use Digia\GraphQL\Execution\ExecutionResult; use Digia\GraphQL\Language\AST\Node\DocumentNode; +use Digia\GraphQL\Language\AST\Node\StringValueNode; use Digia\GraphQL\Language\Source; use Digia\GraphQL\Test\Functional\Language\AbstractParserTest; use Digia\GraphQL\Type\Definition\ObjectType; +use Digia\GraphQL\Type\Definition\ScalarType; use function Digia\GraphQL\Type\GraphQLSchema; use function Digia\GraphQL\Type\GraphQLString; @@ -22,12 +24,21 @@ public function testSimpleMutation() $schema = GraphQLSchema([ 'mutation' => new ObjectType([ - 'name' => 'greeting', + 'name' => 'M', 'fields' => [ - ' message' => [ - 'type' => GraphQLString(), + 'greeting' => [ + 'type' => new ObjectType([ + 'name' => 'GreetingType', + 'fields' => [ + 'message' => [ + 'type' => GraphQLString(), + ] + ], + ]), 'resolve' => function ($name) { - return sprintf("Hello %s. Record was written to database", $name); + return [ + 'message' => sprintf('Hello %s.', $name) + ]; } ] ] @@ -61,7 +72,9 @@ public function testSimpleMutation() ); $expected = new ExecutionResult([ - 'message' => 'Hello Han Solo. Record was written to database' + 'greeting' => [ + 'message' => 'Hello Han Solo.' + ] ], []); $this->assertEquals($expected, $executionResult); From f1cd89ee570a165991e587ae3f965c386e648f1e Mon Sep 17 00:00:00 2001 From: Hung Neo Date: Tue, 27 Feb 2018 13:27:53 +0200 Subject: [PATCH 7/9] Simplify the use of strategy pattern --- src/Execution/Execution.php | 11 ++----- ...ractStrategy.php => ExecutionStrategy.php} | 10 +++--- src/Execution/ExecutorExecutionStrategy.php | 28 +++++++++++++++++ src/Execution/Strategies/MutationStrategy.php | 31 ------------------- src/Execution/Strategies/QueryStrategy.php | 31 ------------------- .../Strategies/SubscriptionStrategy.php | 21 ------------- tests/Functional/Execution/MutationTest.php | 2 -- 7 files changed, 34 insertions(+), 100 deletions(-) rename src/Execution/{Strategies/AbstractStrategy.php => ExecutionStrategy.php} (92%) create mode 100644 src/Execution/ExecutorExecutionStrategy.php delete mode 100644 src/Execution/Strategies/MutationStrategy.php delete mode 100644 src/Execution/Strategies/QueryStrategy.php delete mode 100644 src/Execution/Strategies/SubscriptionStrategy.php diff --git a/src/Execution/Execution.php b/src/Execution/Execution.php index 13121dbb..7f18c459 100644 --- a/src/Execution/Execution.php +++ b/src/Execution/Execution.php @@ -150,15 +150,8 @@ private function executeOperation( OperationDefinitionNode $operation, $rootValue ): ExecutionResult { - $operationName = $context->getOperation()->getOperation(); - - if ($operationName === 'subscription') { - $strategy = new SubscriptionStrategy($context, $operation, $rootValue); - } elseif ($operationName === 'mutation') { - $strategy = new MutationStrategy($context, $operation, $rootValue); - } else { - $strategy = new QueryStrategy($context, $operation, $rootValue); - } + + $strategy = new ExecutorExecutionStrategy($context, $operation, $rootValue); return $strategy->execute(); } diff --git a/src/Execution/Strategies/AbstractStrategy.php b/src/Execution/ExecutionStrategy.php similarity index 92% rename from src/Execution/Strategies/AbstractStrategy.php rename to src/Execution/ExecutionStrategy.php index 71542255..fe89a1a9 100644 --- a/src/Execution/Strategies/AbstractStrategy.php +++ b/src/Execution/ExecutionStrategy.php @@ -1,13 +1,11 @@ getValue()->getValue(); - } elseif ($value instanceof StringValueNode) { + } elseif ($value instanceof InputValueDefinitionNode) { $args[] = $value->getDefaultValue()->getValue(); } } diff --git a/src/Execution/ExecutorExecutionStrategy.php b/src/Execution/ExecutorExecutionStrategy.php new file mode 100644 index 00000000..b84c8cdf --- /dev/null +++ b/src/Execution/ExecutorExecutionStrategy.php @@ -0,0 +1,28 @@ +context->getOperation()->getOperation(); + $schema = $this->context->getSchema(); + + $objectType = ($operation === 'mutation') + ? $schema->getMutation() + : $schema->getQuery(); + + $fields = $this->collectFields($objectType, $this->operation->getSelectionSet(), [], []); + $path = []; + + try { + $data = $this->executeFields($objectType, $this->rootValue, $path, $fields); + } catch (\Exception $ex) { + return new ExecutionResult([], [$ex->getMessage()]); + } + + return new ExecutionResult($data, []); + } +} diff --git a/src/Execution/Strategies/MutationStrategy.php b/src/Execution/Strategies/MutationStrategy.php deleted file mode 100644 index e4f07601..00000000 --- a/src/Execution/Strategies/MutationStrategy.php +++ /dev/null @@ -1,31 +0,0 @@ -context->getSchema()->getMutation(); - $fields = $this->collectFields($mutation, $this->operation->getSelectionSet(), [], []); - $path = []; - - try { - $data = $this->executeFields($mutation, $this->rootValue, $path, $fields); - } catch (\Exception $ex) { - return new ExecutionResult([], [$ex->getMessage()]); - } - - return new ExecutionResult($data, []); - } -} diff --git a/src/Execution/Strategies/QueryStrategy.php b/src/Execution/Strategies/QueryStrategy.php deleted file mode 100644 index e8f76971..00000000 --- a/src/Execution/Strategies/QueryStrategy.php +++ /dev/null @@ -1,31 +0,0 @@ -context->getSchema()->getQuery(); - $fields = $this->collectFields($query, $this->operation->getSelectionSet(), [], []); - $path = []; - - try { - $data = $this->executeFields($query, $this->rootValue, $path, $fields); - } catch (\Exception $ex) { - return new ExecutionResult([], [$ex]); - } - - return new ExecutionResult($data, []); - } -} diff --git a/src/Execution/Strategies/SubscriptionStrategy.php b/src/Execution/Strategies/SubscriptionStrategy.php deleted file mode 100644 index a98b2e9d..00000000 --- a/src/Execution/Strategies/SubscriptionStrategy.php +++ /dev/null @@ -1,21 +0,0 @@ - Date: Tue, 27 Feb 2018 14:39:16 +0200 Subject: [PATCH 8/9] Handle fragments and refactor Execution class --- src/Execution/Execution.php | 31 +-------- src/Execution/ExecutionContext.php | 27 ++++++++ src/Execution/ExecutionStrategy.php | 56 ++++++++++++--- src/Execution/ExecutorExecutionStrategy.php | 10 ++- .../AST/Builder/FragmentSpreadBuilder.php | 7 +- .../AST/Node/FragmentDefinitionNode.php | 2 + src/Language/AST/Node/FragmentSpreadNode.php | 2 + tests/Functional/Execution/MutationTest.php | 68 +++++++++++++++++++ 8 files changed, 160 insertions(+), 43 deletions(-) diff --git a/src/Execution/Execution.php b/src/Execution/Execution.php index 7f18c459..94224d2d 100644 --- a/src/Execution/Execution.php +++ b/src/Execution/Execution.php @@ -3,11 +3,7 @@ namespace Digia\GraphQL\Execution; use Digia\GraphQL\Error\GraphQLError; -use Digia\GraphQL\Execution\Strategies\MutationStrategy; -use Digia\GraphQL\Execution\Strategies\QueryStrategy; -use Digia\GraphQL\Execution\Strategies\SubscriptionStrategy; use Digia\GraphQL\Language\AST\Node\DocumentNode; -use Digia\GraphQL\Language\AST\Node\OperationDefinitionNode; use Digia\GraphQL\Language\AST\NodeKindEnum; use Digia\GraphQL\Type\Schema\Schema; @@ -64,16 +60,13 @@ public static function execute( return new ExecutionResult(null, [$error]); } - $executor = new self($context); + $data = $context->getExecutionStrategy()->execute(); - return $executor->executeOperation( - $context, - $context->getOperation(), - $rootValue - ); + return new ExecutionResult($data, $context->getErrors()); } /** + * @TODO: Consider to create a ExecutionContextBuilder * @param Schema $schema * @param DocumentNode $documentNode * @param $rootValue @@ -137,22 +130,4 @@ private static function buildExecutionContext( return $executionContext; } - - /** - * @param ExecutionContext $context - * @param OperationDefinitionNode $operation - * @param $rootValue - * - * @return ExecutionResult - */ - private function executeOperation( - ExecutionContext $context, - OperationDefinitionNode $operation, - $rootValue - ): ExecutionResult { - - $strategy = new ExecutorExecutionStrategy($context, $operation, $rootValue); - - return $strategy->execute(); - } } diff --git a/src/Execution/ExecutionContext.php b/src/Execution/ExecutionContext.php index d6d19ad6..4562f7fc 100644 --- a/src/Execution/ExecutionContext.php +++ b/src/Execution/ExecutionContext.php @@ -97,6 +97,25 @@ public function getSchema(): Schema return $this->schema; } + /** + * @return array|FragmentDefinitionNode[] + */ + public function getFragments() + { + return $this->fragments; + } + + /** + * Create proper ExecutionStrategy when needed + * + * @return ExecutionStrategy + */ + public function getExecutionStrategy(): ExecutionStrategy + { + //We can probably return different strategy in the future e.g:AsyncExecutionStrategy + return new ExecutorExecutionStrategy($this, $this->operation, $this->rootValue); + } + /** * @param GraphQLError $error * @return ExecutionContext @@ -106,4 +125,12 @@ public function addError(GraphQLError $error) $this->errors[] = $error; return $this; } + + /** + * @return array|GraphQLError[] + */ + public function getErrors() + { + return $this->errors; + } } diff --git a/src/Execution/ExecutionStrategy.php b/src/Execution/ExecutionStrategy.php index fe89a1a9..88e72daf 100644 --- a/src/Execution/ExecutionStrategy.php +++ b/src/Execution/ExecutionStrategy.php @@ -5,10 +5,10 @@ use Digia\GraphQL\Error\GraphQLError; use Digia\GraphQL\Language\AST\Node\ArgumentNode; use Digia\GraphQL\Language\AST\Node\FieldNode; +use Digia\GraphQL\Language\AST\Node\FragmentDefinitionNode; use Digia\GraphQL\Language\AST\Node\InputValueDefinitionNode; use Digia\GraphQL\Language\AST\Node\OperationDefinitionNode; use Digia\GraphQL\Language\AST\Node\SelectionSetNode; -use Digia\GraphQL\Language\AST\Node\StringValueNode; use Digia\GraphQL\Language\AST\NodeKindEnum; use Digia\GraphQL\Type\Definition\ObjectType; @@ -49,7 +49,10 @@ public function __construct( $this->rootValue = $rootValue; } - abstract function execute(): ExecutionResult; + /** + * @return array|null + */ + abstract function execute(): ?array; /** * @param ObjectType $runtimeType @@ -65,11 +68,31 @@ protected function collectFields( $visitedFragmentNames ) { foreach ($selectionSet->getSelections() as $selection) { + /** @var FieldNode $selection */ switch ($selection->getKind()) { case NodeKindEnum::FIELD: - /** @var FieldNode $selection */ - $name = $selection->getName()->getValue(); - $fields[$name][] = $selection; + $fields[$selection->getNameValue()][] = $selection; + break; + case NodeKindEnum::INLINE_FRAGMENT: + //TODO check if should include this node + $this->collectFields( + $runtimeType, + $selection->getSelectionSet(), + $fields, + $visitedFragmentNames + ); + break; + case NodeKindEnum::FRAGMENT_SPREAD: + //TODO check if should include this node + $visitedFragmentNames[$selection->getNameValue()] = true; + /** @var FragmentDefinitionNode $fragment */ + $fragment = $this->context->getFragments()[$selection->getNameValue()]; + $this->collectFields( + $runtimeType, + $fragment->getSelectionSet(), + $fields, + $visitedFragmentNames + ); break; } } @@ -95,9 +118,13 @@ protected function executeFields( $fields): array { $finalResults = []; - foreach ($fields as $responseName => $fieldNodes) { + foreach ($fields as $fieldName => $fieldNodes) { $fieldPath = $path; - $fieldPath[] = $responseName; + $fieldPath[] = $fieldName; + + if (!$this->isDefinedField($parentType, $fieldName)) { + continue; + } $result = $this->resolveField($parentType, $source, @@ -105,12 +132,23 @@ protected function executeFields( $fieldPath ); - $finalResults[$responseName] = $result; + $finalResults[$fieldName] = $result; } return $finalResults; } + /** + * @param ObjectType $parentType + * @param string $fieldName + * @return bool + * @throws \Exception + */ + protected function isDefinedField(ObjectType $parentType, string $fieldName) + { + return isset($parentType->getFields()[$fieldName]); + } + /** * @param ObjectType $parentType * @param $source @@ -130,7 +168,7 @@ protected function resolveField( /** @var FieldNode $fieldNode */ $fieldNode = $fieldNodes[0]; - $field = $parentType->getFields()[$fieldNode->getName()->getValue()]; + $field = $parentType->getFields()[$fieldNode->getNameValue()]; $inputValues = $fieldNode->getArguments() ?? []; diff --git a/src/Execution/ExecutorExecutionStrategy.php b/src/Execution/ExecutorExecutionStrategy.php index b84c8cdf..125b7216 100644 --- a/src/Execution/ExecutorExecutionStrategy.php +++ b/src/Execution/ExecutorExecutionStrategy.php @@ -5,7 +5,10 @@ class ExecutorExecutionStrategy extends ExecutionStrategy { - function execute(): ExecutionResult + /** + * @return ?array + */ + function execute(): ?array { $operation = $this->context->getOperation()->getOperation(); $schema = $this->context->getSchema(); @@ -20,9 +23,10 @@ function execute(): ExecutionResult try { $data = $this->executeFields($objectType, $this->rootValue, $path, $fields); } catch (\Exception $ex) { - return new ExecutionResult([], [$ex->getMessage()]); + $this->context->addError($ex); + return null; } - return new ExecutionResult($data, []); + return $data; } } diff --git a/src/Language/AST/Builder/FragmentSpreadBuilder.php b/src/Language/AST/Builder/FragmentSpreadBuilder.php index b7ade45d..96652c36 100644 --- a/src/Language/AST/Builder/FragmentSpreadBuilder.php +++ b/src/Language/AST/Builder/FragmentSpreadBuilder.php @@ -15,9 +15,10 @@ class FragmentSpreadBuilder extends AbstractBuilder public function build(array $ast): NodeInterface { return new FragmentSpreadNode([ - 'name' => $this->buildOne($ast, 'name'), - 'directives' => $this->buildMany($ast, 'directives'), - 'location' => $this->createLocation($ast), + 'name' => $this->buildOne($ast, 'name'), + 'directives' => $this->buildMany($ast, 'directives'), + 'selectionSet' => $this->buildOne($ast, 'selectionSet'), + 'location' => $this->createLocation($ast), ]); } diff --git a/src/Language/AST/Node/FragmentDefinitionNode.php b/src/Language/AST/Node/FragmentDefinitionNode.php index dbfe217f..f00d4543 100644 --- a/src/Language/AST/Node/FragmentDefinitionNode.php +++ b/src/Language/AST/Node/FragmentDefinitionNode.php @@ -2,6 +2,7 @@ namespace Digia\GraphQL\Language\AST\Node; +use Digia\GraphQL\Language\AST\Node\Behavior\SelectionSetTrait; use Digia\GraphQL\Language\AST\NodeKindEnum; use Digia\GraphQL\Language\AST\Node\Behavior\NameTrait; use Digia\GraphQL\Language\AST\Node\Behavior\TypeConditionTrait; @@ -14,6 +15,7 @@ class FragmentDefinitionNode extends AbstractNode implements ExecutableDefinitio use NameTrait; use TypeConditionTrait; use VariableDefinitionsTrait; + use SelectionSetTrait; /** * @var string diff --git a/src/Language/AST/Node/FragmentSpreadNode.php b/src/Language/AST/Node/FragmentSpreadNode.php index 5a595f4b..dc2e00f2 100644 --- a/src/Language/AST/Node/FragmentSpreadNode.php +++ b/src/Language/AST/Node/FragmentSpreadNode.php @@ -2,6 +2,7 @@ namespace Digia\GraphQL\Language\AST\Node; +use Digia\GraphQL\Language\AST\Node\Behavior\SelectionSetTrait; use Digia\GraphQL\Language\AST\NodeKindEnum; use Digia\GraphQL\Language\AST\Node\Behavior\DirectivesTrait; use Digia\GraphQL\Language\AST\Node\Behavior\NameTrait; @@ -12,6 +13,7 @@ class FragmentSpreadNode extends AbstractNode implements NodeInterface use NameTrait; use DirectivesTrait; + use SelectionSetTrait; /** * @var string diff --git a/tests/Functional/Execution/MutationTest.php b/tests/Functional/Execution/MutationTest.php index e15f600c..7639d49a 100644 --- a/tests/Functional/Execution/MutationTest.php +++ b/tests/Functional/Execution/MutationTest.php @@ -10,6 +10,7 @@ use Digia\GraphQL\Type\Definition\ObjectType; use function Digia\GraphQL\Type\GraphQLSchema; use function Digia\GraphQL\Type\GraphQLString; +use Digia\GraphQL\Type\Schema\Schema; class MutationTest extends AbstractParserTest { @@ -77,4 +78,71 @@ public function testSimpleMutation() $this->assertEquals($expected, $executionResult); } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testDoesNotIncludeIllegalFieldsInOutput() + { + /** @var DocumentNode $documentNode */ + $documentNode = $this->parser->parse(new Source(' + mutation M { + thisIsIllegalDontIncludeMe + }' + )); + + $schema = GraphQLSchema([ + 'mutation' => + new ObjectType([ + 'name' => 'M', + 'fields' => [ + 'd' => [ + 'type' => GraphQLString(), + 'resolve' => function () { + return 'd'; + } + ] + ] + ]) + ]); + + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Q', + 'fields' => [ + 'a' => ['type' => GraphQLString()], + ] + ]), + 'mutation' => new ObjectType([ + 'name' => 'M', + 'fields' => [ + 'c' => ['type' => GraphQLString()], + ] + ]) + ]); + + + $rootValue = []; + $contextValue = ''; + $variableValues = []; + $operationName = 'M'; + $fieldResolver = null; + + + /** @var ExecutionResult $executionResult */ + $executionResult = Execution::execute( + $schema, + $documentNode, + $rootValue, + $contextValue, + $variableValues, + $operationName, + $fieldResolver + ); + + $expected = new ExecutionResult([], []); + + $this->assertEquals($expected, $executionResult); + } } From 6895692dd97f13eb822cc5ebb811ff38184a1976 Mon Sep 17 00:00:00 2001 From: Hung Neo Date: Tue, 27 Feb 2018 16:31:26 +0200 Subject: [PATCH 9/9] Add fragment tests --- src/Execution/Execution.php | 6 +- src/Execution/ExecutionResult.php | 8 ++ src/Execution/ExecutionStrategy.php | 32 ++++++- src/Execution/ExecutorExecutionStrategy.php | 8 +- tests/Functional/Execution/ExecutionTest.php | 93 +++++++++++++++++++- 5 files changed, 136 insertions(+), 11 deletions(-) diff --git a/src/Execution/Execution.php b/src/Execution/Execution.php index 94224d2d..c95281d2 100644 --- a/src/Execution/Execution.php +++ b/src/Execution/Execution.php @@ -57,7 +57,7 @@ public static function execute( $fieldResolver ); } catch (GraphQLError $error) { - return new ExecutionResult(null, [$error]); + return new ExecutionResult(['data' => null], [$error]); } $data = $context->getExecutionStrategy()->execute(); @@ -107,12 +107,12 @@ private static function buildExecutionContext( } break; case NodeKindEnum::FRAGMENT_DEFINITION: + case NodeKindEnum::FRAGMENT_SPREAD: $fragments[$definition->getName()->getValue()] = $definition; break; default: throw new GraphQLError( - "GraphQL cannot execute a request containing a {$definition->getKind()}.", - [$definition] + "GraphQL cannot execute a request containing a {$definition->getKind()}." ); } } diff --git a/src/Execution/ExecutionResult.php b/src/Execution/ExecutionResult.php index 7eda2434..413f128c 100644 --- a/src/Execution/ExecutionResult.php +++ b/src/Execution/ExecutionResult.php @@ -27,6 +27,14 @@ public function __construct(array $data, array $errors) $this->data = $data; } + /** + * @return array|GraphQLError[] + */ + public function getErrors(): array + { + return $this->errors; + } + /** * @param GraphQLError $error * @return ExecutionResult diff --git a/src/Execution/ExecutionStrategy.php b/src/Execution/ExecutionStrategy.php index 88e72daf..70b2fd90 100644 --- a/src/Execution/ExecutionStrategy.php +++ b/src/Execution/ExecutionStrategy.php @@ -59,7 +59,7 @@ abstract function execute(): ?array; * @param SelectionSetNode $selectionSet * @param $fields * @param $visitedFragmentNames - * @return mixed + * @return \ArrayObject */ protected function collectFields( ObjectType $runtimeType, @@ -71,7 +71,11 @@ protected function collectFields( /** @var FieldNode $selection */ switch ($selection->getKind()) { case NodeKindEnum::FIELD: - $fields[$selection->getNameValue()][] = $selection; + $name = $selection->getNameValue(); + if (!isset($fields[$name])) { + $fields[$name] = new \ArrayObject(); + } + $fields[$name][] = $selection; break; case NodeKindEnum::INLINE_FRAGMENT: //TODO check if should include this node @@ -84,6 +88,9 @@ protected function collectFields( break; case NodeKindEnum::FRAGMENT_SPREAD: //TODO check if should include this node + if (!empty($visitedFragmentNames[$selection->getNameValue()])) { + continue; + } $visitedFragmentNames[$selection->getNameValue()] = true; /** @var FragmentDefinitionNode $fragment */ $fragment = $this->context->getFragments()[$selection->getNameValue()]; @@ -96,6 +103,7 @@ protected function collectFields( break; } } + return $fields; } @@ -118,10 +126,10 @@ protected function executeFields( $fields): array { $finalResults = []; + foreach ($fields as $fieldName => $fieldNodes) { $fieldPath = $path; $fieldPath[] = $fieldName; - if (!$this->isDefinedField($parentType, $fieldName)) { continue; } @@ -181,6 +189,22 @@ protected function resolveField( $args[] = $value->getDefaultValue()->getValue(); } } - return $field->resolve(...$args); + + $result = $field->resolve(...$args); + + //TODO Resolve sub fields + + return $result; + } + + private function completeValue( + $returnType, + $fieldNodes, + $info, + $path, + &$result) + { + + } } diff --git a/src/Execution/ExecutorExecutionStrategy.php b/src/Execution/ExecutorExecutionStrategy.php index 125b7216..5f1ea23e 100644 --- a/src/Execution/ExecutorExecutionStrategy.php +++ b/src/Execution/ExecutorExecutionStrategy.php @@ -2,6 +2,8 @@ namespace Digia\GraphQL\Execution; +use Digia\GraphQL\Error\GraphQLError; + class ExecutorExecutionStrategy extends ExecutionStrategy { @@ -17,13 +19,15 @@ function execute(): ?array ? $schema->getMutation() : $schema->getQuery(); - $fields = $this->collectFields($objectType, $this->operation->getSelectionSet(), [], []); + $fields = $this->collectFields($objectType, $this->operation->getSelectionSet(), new \ArrayObject(), new \ArrayObject()); $path = []; try { $data = $this->executeFields($objectType, $this->rootValue, $path, $fields); } catch (\Exception $ex) { - $this->context->addError($ex); + $this->context->addError( + new GraphQLError($ex->getMessage()) + ); return null; } diff --git a/tests/Functional/Execution/ExecutionTest.php b/tests/Functional/Execution/ExecutionTest.php index e7144916..0153e486 100644 --- a/tests/Functional/Execution/ExecutionTest.php +++ b/tests/Functional/Execution/ExecutionTest.php @@ -15,14 +15,14 @@ use Digia\GraphQL\Language\Location; use Digia\GraphQL\Language\Source; use Digia\GraphQL\Language\SourceLocation; -use Digia\GraphQL\Test\TestCase; +use Digia\GraphQL\Test\Functional\Language\AbstractParserTest; use Digia\GraphQL\Type\Definition\ObjectType; use Digia\GraphQL\Type\Schema\Schema; use function Digia\GraphQL\Type\GraphQLInt; use function Digia\GraphQL\Type\GraphQLList; use function Digia\GraphQL\Type\GraphQLString; -class ExecutionTest extends TestCase +class ExecutionTest extends AbstractParserTest { /** @@ -317,4 +317,93 @@ public function testExecuteQueryWithMultipleFields() $this->assertEquals($expected, $executionResult); } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testHandleFragments() + { + $documentNode = $this->parser->parse(new Source(' + { a, ...FragOne, ...FragTwo } + + fragment FragOne on Type { + b + deep { b, deeper: deep { b } } + } + + fragment FragTwo on Type { + c + deep { c, deeper: deep { c } } + }')); + + $Type = new ObjectType([ + 'name' => 'Type', + 'fields' => function() use (&$Type) { + return [ + 'a' => [ + 'type' => GraphQLString(), + 'resolve' => function () { + return 'Apple'; + } + ], + 'b' => [ + 'type' => GraphQLString(), + 'resolve' => function () { + return 'Banana'; + } + ], + 'c' => [ + 'type' => GraphQLString(), + 'resolve' => function () { + return 'Cherry'; + } + ], + 'deep' => [ + 'type' => $Type, + 'resolve' => function () { + return []; + } + ] + ]; + } + ]); + + $schema = new Schema([ + 'query' => $Type + ]); + + $rootValue = []; + $contextValue = ''; + $variableValues = []; + $operationName = ''; + $fieldResolver = null; + + /** @var ExecutionResult $executionResult */ + $executionResult = Execution::execute( + $schema, + $documentNode, + $rootValue, + $contextValue, + $variableValues, + $operationName, + $fieldResolver + ); + + $expected = new ExecutionResult([ + 'a' => 'Apple', + 'b' => 'Banana', + 'c' => 'Cherry', + 'deep' => [ +// 'b' => 'Banana', +// 'c' => 'Cherry', +// 'deeper' => [ +// 'b' => 'Banana', +// 'c' => 'Cherry' +// ] + ] + ], []); + + $this->assertEquals($expected, $executionResult); + } }