From 2cbccb87db75f6b68ceb0dbb7997b3821c4ee065 Mon Sep 17 00:00:00 2001 From: Daniel Tschinder Date: Fri, 9 Feb 2018 16:14:04 +0100 Subject: [PATCH] Remove duplicated code from buildASTSchema and extendSchema ref: graphql/graphql-js#1000 BREAKING CHANGE: SchemaBuilder::build() and buildAST() and constructor removed the typedecorator, as not needed anymore as library can now resolve union and interfaces from generated schemas. --- src/Utils/ASTDefinitionBuilder.php | 437 +++++++++++++++++++++++++++ src/Utils/BuildSchema.php | 465 ++--------------------------- tests/Utils/BuildSchemaTest.php | 129 +------- 3 files changed, 461 insertions(+), 570 deletions(-) create mode 100644 src/Utils/ASTDefinitionBuilder.php diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php new file mode 100644 index 0000000..7541633 --- /dev/null +++ b/src/Utils/ASTDefinitionBuilder.php @@ -0,0 +1,437 @@ +typeDefintionsMap = $typeDefintionsMap; + $this->options = $options; + $this->resolveType = $resolveType; + + $this->cache = [ + 'String' => Type::string(), + 'Int' => Type::int(), + 'Float' => Type::float(), + 'Boolean' => Type::boolean(), + 'ID' => Type::id(), + '__Schema' => Introspection::_schema(), + '__Directive' => Introspection::_directive(), + '__DirectiveLocation' => Introspection::_directiveLocation(), + '__Type' => Introspection::_type(), + '__Field' => Introspection::_field(), + '__InputValue' => Introspection::_inputValue(), + '__EnumValue' => Introspection::_enumValue(), + '__TypeKind' => Introspection::_typeKind(), + ]; + } + + /** + * @param Type $innerType + * @param TypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode + * @return Type + */ + private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode) + { + if ($inputTypeNode->kind == NodeKind::LIST_TYPE) { + return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type)); + } + if ($inputTypeNode->kind == NodeKind::NON_NULL_TYPE) { + $wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type); + Utils::invariant(!($wrappedType instanceof NonNull), 'No nesting nonnull.'); + return Type::nonNull($wrappedType); + } + return $innerType; + } + + /** + * @param TypeNode|ListTypeNode|NonNullTypeNode $typeNode + * @return TypeNode + */ + private function getNamedTypeNode(TypeNode $typeNode) + { + $namedType = $typeNode; + while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) { + $namedType = $namedType->type; + } + return $namedType; + } + + /** + * @param string $typeName + * @param NamedTypeNode|null $typeNode + * @return Type + * @throws Error + */ + private function internalBuildType($typeName, $typeNode = null) { + if (!isset($this->cache[$typeName])) { + if (isset($this->typeDefintionsMap[$typeName])) { + $this->cache[$typeName] = $this->makeSchemaDef($this->typeDefintionsMap[$typeName]); + } else { + $fn = $this->resolveType; + $this->cache[$typeName] = $fn($typeName, $typeNode); + } + } + + return $this->cache[$typeName]; + } + + /** + * @param string|NamedTypeNode $ref + * @return Type + * @throws Error + */ + public function buildType($ref) + { + if (is_string($ref)) { + return $this->internalBuildType($ref); + } + + return $this->internalBuildType($ref->name->value, $ref); + } + + /** + * @param TypeNode $typeNode + * @return InputType|Type + * @throws Error + */ + public function buildInputType(TypeNode $typeNode) + { + $type = $this->internalBuildWrappedType($typeNode); + Utils::invariant(Type::isInputType($type), 'Expected Input type.'); + return $type; + } + + /** + * @param TypeNode $typeNode + * @return OutputType|Type + * @throws Error + */ + public function buildOutputType(TypeNode $typeNode) + { + $type = $this->internalBuildWrappedType($typeNode); + Utils::invariant(Type::isOutputType($type), 'Expected Output type.'); + return $type; + } + + /** + * @param TypeNode|string $typeNode + * @return ObjectType|Type + * @throws Error + */ + public function buildObjectType($typeNode) + { + $type = $this->buildType($typeNode); + Utils::invariant($type instanceof ObjectType, 'Expected Object type.' . get_class($type)); + return $type; + } + + /** + * @param TypeNode|string $typeNode + * @return InterfaceType|Type + * @throws Error + */ + public function buildInterfaceType($typeNode) + { + $type = $this->buildType($typeNode); + Utils::invariant($type instanceof InterfaceType, 'Expected Interface type.'); + return $type; + } + + /** + * @param TypeNode $typeNode + * @return Type + * @throws Error + */ + private function internalBuildWrappedType(TypeNode $typeNode) + { + $typeDef = $this->buildType($this->getNamedTypeNode($typeNode)); + return $this->buildWrappedType($typeDef, $typeNode); + } + + public function buildDirective(DirectiveDefinitionNode $directiveNode) + { + return new Directive([ + 'name' => $directiveNode->name->value, + 'description' => $this->getDescription($directiveNode), + 'locations' => Utils::map($directiveNode->locations, function ($node) { + return $node->value; + }), + 'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null, + 'astNode' => $directiveNode, + ]); + } + + public function buildField(FieldDefinitionNode $field) + { + return [ + 'type' => $this->buildOutputType($field->type), + 'description' => $this->getDescription($field), + 'args' => $this->makeInputValues($field->arguments), + 'deprecationReason' => $this->getDeprecationReason($field), + 'astNode' => $field + ]; + } + + private function makeSchemaDef($def) + { + if (!$def) { + throw new Error('def must be defined.'); + } + switch ($def->kind) { + case NodeKind::OBJECT_TYPE_DEFINITION: + return $this->makeTypeDef($def); + case NodeKind::INTERFACE_TYPE_DEFINITION: + return $this->makeInterfaceDef($def); + case NodeKind::ENUM_TYPE_DEFINITION: + return $this->makeEnumDef($def); + case NodeKind::UNION_TYPE_DEFINITION: + return $this->makeUnionDef($def); + case NodeKind::SCALAR_TYPE_DEFINITION: + return $this->makeScalarDef($def); + case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: + return $this->makeInputObjectDef($def); + default: + throw new Error("Type kind of {$def->kind} not supported."); + } + } + + private function makeTypeDef(ObjectTypeDefinitionNode $def) + { + $typeName = $def->name->value; + return new ObjectType([ + 'name' => $typeName, + 'description' => $this->getDescription($def), + 'fields' => function () use ($def) { + return $this->makeFieldDefMap($def); + }, + 'interfaces' => function () use ($def) { + return $this->makeImplementedInterfaces($def); + }, + 'astNode' => $def + ]); + } + + private function makeFieldDefMap($def) + { + return Utils::keyValMap( + $def->fields, + function ($field) { + return $field->name->value; + }, + function ($field) { + return $this->buildField($field); + } + ); + } + + private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def) + { + if (isset($def->interfaces)) { + return Utils::map($def->interfaces, function ($iface) { + return $this->buildInterfaceType($iface); + }); + } + return null; + } + + private function makeInputValues($values) + { + return Utils::keyValMap( + $values, + function ($value) { + return $value->name->value; + }, + function ($value) { + $type = $this->buildInputType($value->type); + $config = [ + 'name' => $value->name->value, + 'type' => $type, + 'description' => $this->getDescription($value), + 'astNode' => $value + ]; + if (isset($value->defaultValue)) { + $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type); + } + return $config; + } + ); + } + + private function makeInterfaceDef(InterfaceTypeDefinitionNode $def) + { + $typeName = $def->name->value; + return new InterfaceType([ + 'name' => $typeName, + 'description' => $this->getDescription($def), + 'fields' => function () use ($def) { + return $this->makeFieldDefMap($def); + }, + 'astNode' => $def + ]); + } + + private function makeEnumDef(EnumTypeDefinitionNode $def) + { + return new EnumType([ + 'name' => $def->name->value, + 'description' => $this->getDescription($def), + 'astNode' => $def, + 'values' => Utils::keyValMap( + $def->values, + function ($enumValue) { + return $enumValue->name->value; + }, + function ($enumValue) { + return [ + 'description' => $this->getDescription($enumValue), + 'deprecationReason' => $this->getDeprecationReason($enumValue), + 'astNode' => $enumValue + ]; + } + ) + ]); + } + + private function makeUnionDef(UnionTypeDefinitionNode $def) + { + return new UnionType([ + 'name' => $def->name->value, + 'description' => $this->getDescription($def), + 'types' => Utils::map($def->types, function ($typeNode) { + return $this->buildObjectType($typeNode); + }), + 'astNode' => $def + ]); + } + + private function makeScalarDef(ScalarTypeDefinitionNode $def) + { + return new CustomScalarType([ + 'name' => $def->name->value, + 'description' => $this->getDescription($def), + 'astNode' => $def, + 'serialize' => function($value) { + return $value; + }, + ]); + } + + private function makeInputObjectDef(InputObjectTypeDefinitionNode $def) + { + return new InputObjectType([ + 'name' => $def->name->value, + 'description' => $this->getDescription($def), + 'fields' => function () use ($def) { + return $this->makeInputValues($def->fields); + }, + 'astNode' => $def, + ]); + } + + /** + * Given a collection of directives, returns the string value for the + * deprecation reason. + * + * @param EnumValueDefinitionNode | FieldDefinitionNode $node + * @return string + */ + private function getDeprecationReason($node) + { + $deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node); + return isset($deprecated['reason']) ? $deprecated['reason'] : null; + } + + /** + * Given an ast node, returns its string description. + */ + private function getDescription($node) + { + if ($node->description) { + return $node->description->value; + } + if (isset($this->options['commentDescriptions'])) { + $rawValue = $this->getLeadingCommentBlock($node); + if ($rawValue !== null) { + return BlockString::value("\n" . $rawValue); + } + } + + return null; + } + + private function getLeadingCommentBlock($node) + { + $loc = $node->loc; + if (!$loc || !$loc->startToken) { + return; + } + $comments = []; + $token = $loc->startToken->prev; + while ( + $token && + $token->kind === Token::COMMENT && + $token->next && $token->prev && + $token->line + 1 === $token->next->line && + $token->line !== $token->prev->line + ) { + $value = $token->value; + $comments[] = $value; + $token = $token->prev; + } + + return implode("\n", array_reverse($comments)); + } +} diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index b04ef40..9d764cd 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -2,35 +2,13 @@ namespace GraphQL\Utils; use GraphQL\Error\Error; -use GraphQL\Executor\Values; -use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DocumentNode; -use GraphQL\Language\AST\EnumTypeDefinitionNode; -use GraphQL\Language\AST\EnumValueDefinitionNode; -use GraphQL\Language\AST\FieldDefinitionNode; -use GraphQL\Language\AST\InputObjectTypeDefinitionNode; -use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\AST\ObjectTypeDefinitionNode; -use GraphQL\Language\AST\ScalarTypeDefinitionNode; use GraphQL\Language\AST\SchemaDefinitionNode; -use GraphQL\Language\AST\TypeNode; -use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\Parser; use GraphQL\Language\Source; -use GraphQL\Language\Token; use GraphQL\Type\Schema; use GraphQL\Type\Definition\Directive; -use GraphQL\Type\Definition\EnumType; -use GraphQL\Type\Definition\InputObjectType; -use GraphQL\Type\Definition\InterfaceType; -use GraphQL\Type\Definition\FieldArgument; -use GraphQL\Type\Definition\NonNull; -use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\CustomScalarType; -use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\UnionType; -use GraphQL\Type\Introspection; /** * Build instance of `GraphQL\Type\Schema` out of type language definition (string or parsed AST) @@ -38,33 +16,6 @@ use GraphQL\Type\Introspection; */ class BuildSchema { - /** - * @param Type $innerType - * @param TypeNode $inputTypeNode - * @return Type - */ - private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode) - { - if ($inputTypeNode->kind == NodeKind::LIST_TYPE) { - return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type)); - } - if ($inputTypeNode->kind == NodeKind::NON_NULL_TYPE) { - $wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type); - Utils::invariant(!($wrappedType instanceof NonNull), 'No nesting nonnull.'); - return Type::nonNull($wrappedType); - } - return $innerType; - } - - private function getNamedTypeNode(TypeNode $typeNode) - { - $namedType = $typeNode; - while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) { - $namedType = $namedType->type; - } - return $namedType; - } - /** * This takes the ast of a schema document produced by the parse function in * GraphQL\Language\Parser. @@ -75,7 +26,7 @@ class BuildSchema * Given that AST it constructs a GraphQL\Type\Schema. The resulting schema * has no resolve methods, so execution will use default resolvers. * - * Accepts options as a third argument: + * Accepts options as a second argument: * * - commentDescriptions: * Provide true to use preceding comments as the description. @@ -83,27 +34,24 @@ class BuildSchema * * @api * @param DocumentNode $ast - * @param callable $typeConfigDecorator + * @param array $options * @return Schema * @throws Error */ - public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = []) + public static function buildAST(DocumentNode $ast, array $options = []) { - $builder = new self($ast, $typeConfigDecorator, $options); + $builder = new self($ast, $options); return $builder->buildSchema(); } private $ast; - private $innerTypeMap; private $nodeMap; - private $typeConfigDecorator; private $loadedTypeDefs; private $options; - public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = []) + public function __construct(DocumentNode $ast, array $options = []) { $this->ast = $ast; - $this->typeConfigDecorator = $typeConfigDecorator; $this->loadedTypeDefs = []; $this->options = $options; } @@ -150,23 +98,15 @@ class BuildSchema 'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null, ]; - $this->innerTypeMap = [ - 'String' => Type::string(), - 'Int' => Type::int(), - 'Float' => Type::float(), - 'Boolean' => Type::boolean(), - 'ID' => Type::id(), - '__Schema' => Introspection::_schema(), - '__Directive' => Introspection::_directive(), - '__DirectiveLocation' => Introspection::_directiveLocation(), - '__Type' => Introspection::_type(), - '__Field' => Introspection::_field(), - '__InputValue' => Introspection::_inputValue(), - '__EnumValue' => Introspection::_enumValue(), - '__TypeKind' => Introspection::_typeKind(), - ]; + $defintionBuilder = new ASTDefinitionBuilder( + $this->nodeMap, + $this->options, + function($typeName) { throw new Error('Type "'. $typeName . '" not found in document.'); } + ); - $directives = array_map([$this, 'getDirective'], $directiveDefs); + $directives = array_map(function($def) use ($defintionBuilder) { + return $defintionBuilder->buildDirective($def); + }, $directiveDefs); // If specified directives were not explicitly declared, add them. $skip = array_reduce($directives, function ($hasSkip, $directive) { @@ -197,23 +137,23 @@ class BuildSchema } $schema = new Schema([ - 'query' => $this->getObjectType($operationTypes['query']), + 'query' => $defintionBuilder->buildObjectType($operationTypes['query']), 'mutation' => isset($operationTypes['mutation']) ? - $this->getObjectType($operationTypes['mutation']) : + $defintionBuilder->buildObjectType($operationTypes['mutation']) : null, 'subscription' => isset($operationTypes['subscription']) ? - $this->getObjectType($operationTypes['subscription']) : + $defintionBuilder->buildObjectType($operationTypes['subscription']) : null, - 'typeLoader' => function ($name) { - return $this->typeDefNamed($name); + 'typeLoader' => function ($name) use ($defintionBuilder) { + return $defintionBuilder->buildType($name); }, 'directives' => $directives, 'astNode' => $schemaDef, - 'types' => function () { + 'types' => function () use ($defintionBuilder) { $types = []; foreach ($this->nodeMap as $name => $def) { if (!isset($this->loadedTypeDefs[$name])) { - $types[] = $this->typeDefNamed($def->name->value); + $types[] = $defintionBuilder->buildType($def->name->value); } } return $types; @@ -250,377 +190,18 @@ class BuildSchema return $opTypes; } - private function getDirective(DirectiveDefinitionNode $directiveNode) - { - return new Directive([ - 'name' => $directiveNode->name->value, - 'description' => $this->getDescription($directiveNode), - 'locations' => Utils::map($directiveNode->locations, function ($node) { - return $node->value; - }), - 'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null, - 'astNode' => $directiveNode - ]); - } - - /** - * @param string $name - * @return CustomScalarType|EnumType|InputObjectType|UnionType - * @throws Error - */ - private function getObjectType($name) - { - $type = $this->typeDefNamed($name); - Utils::invariant( - $type instanceof ObjectType, - 'AST must provide object type.' - ); - return $type; - } - - private function produceType(TypeNode $typeNode) - { - $typeName = $this->getNamedTypeNode($typeNode)->name->value; - $typeDef = $this->typeDefNamed($typeName); - return $this->buildWrappedType($typeDef, $typeNode); - } - - private function produceInputType(TypeNode $typeNode) - { - $type = $this->produceType($typeNode); - Utils::invariant(Type::isInputType($type), 'Expected Input type.'); - return $type; - } - - private function produceOutputType(TypeNode $typeNode) - { - $type = $this->produceType($typeNode); - Utils::invariant(Type::isOutputType($type), 'Expected Input type.'); - return $type; - } - - private function produceObjectType(TypeNode $typeNode) - { - $type = $this->produceType($typeNode); - Utils::invariant($type instanceof ObjectType, 'Expected Object type.'); - return $type; - } - - private function produceInterfaceType(TypeNode $typeNode) - { - $type = $this->produceType($typeNode); - Utils::invariant($type instanceof InterfaceType, 'Expected Interface type.'); - return $type; - } - - private function typeDefNamed($typeName) - { - if (isset($this->innerTypeMap[$typeName])) { - return $this->innerTypeMap[$typeName]; - } - - if (!isset($this->nodeMap[$typeName])) { - throw new Error('Type "' . $typeName . '" not found in document.'); - } - - $this->loadedTypeDefs[$typeName] = true; - - $config = $this->makeSchemaDefConfig($this->nodeMap[$typeName]); - - if ($this->typeConfigDecorator) { - $fn = $this->typeConfigDecorator; - try { - $config = $fn($config, $this->nodeMap[$typeName], $this->nodeMap); - } catch (\Exception $e) { - throw new Error( - "Type config decorator passed to " . (static::class) . " threw an error " . - "when building $typeName type: {$e->getMessage()}", - null, - null, - null, - null, - $e - ); - } catch (\Throwable $e) { - throw new Error( - "Type config decorator passed to " . (static::class) . " threw an error " . - "when building $typeName type: {$e->getMessage()}", - null, - null, - null, - null, - $e - ); - } - if (!is_array($config) || isset($config[0])) { - throw new Error( - "Type config decorator passed to " . (static::class) . " is expected to return an array, but got " . - Utils::getVariableType($config) - ); - } - } - - $innerTypeDef = $this->makeSchemaDef($this->nodeMap[$typeName], $config); - - if (!$innerTypeDef) { - throw new Error("Nothing constructed for $typeName."); - } - $this->innerTypeMap[$typeName] = $innerTypeDef; - return $innerTypeDef; - } - - private function makeSchemaDefConfig($def) - { - if (!$def) { - throw new Error('def must be defined.'); - } - switch ($def->kind) { - case NodeKind::OBJECT_TYPE_DEFINITION: - return $this->makeTypeDefConfig($def); - case NodeKind::INTERFACE_TYPE_DEFINITION: - return $this->makeInterfaceDefConfig($def); - case NodeKind::ENUM_TYPE_DEFINITION: - return $this->makeEnumDefConfig($def); - case NodeKind::UNION_TYPE_DEFINITION: - return $this->makeUnionDefConfig($def); - case NodeKind::SCALAR_TYPE_DEFINITION: - return $this->makeScalarDefConfig($def); - case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: - return $this->makeInputObjectDefConfig($def); - default: - throw new Error("Type kind of {$def->kind} not supported."); - } - } - - private function makeSchemaDef($def, array $config = null) - { - if (!$def) { - throw new Error('def must be defined.'); - } - - $config = $config ?: $this->makeSchemaDefConfig($def); - - switch ($def->kind) { - case NodeKind::OBJECT_TYPE_DEFINITION: - return new ObjectType($config); - case NodeKind::INTERFACE_TYPE_DEFINITION: - return new InterfaceType($config); - case NodeKind::ENUM_TYPE_DEFINITION: - return new EnumType($config); - case NodeKind::UNION_TYPE_DEFINITION: - return new UnionType($config); - case NodeKind::SCALAR_TYPE_DEFINITION: - return new CustomScalarType($config); - case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: - return new InputObjectType($config); - default: - throw new Error("Type kind of {$def->kind} not supported."); - } - } - - private function makeTypeDefConfig(ObjectTypeDefinitionNode $def) - { - $typeName = $def->name->value; - return [ - 'name' => $typeName, - 'description' => $this->getDescription($def), - 'fields' => function () use ($def) { - return $this->makeFieldDefMap($def); - }, - 'interfaces' => function () use ($def) { - return $this->makeImplementedInterfaces($def); - }, - 'astNode' => $def - ]; - } - - private function makeFieldDefMap($def) - { - return Utils::keyValMap( - $def->fields, - function ($field) { - return $field->name->value; - }, - function ($field) { - return [ - 'type' => $this->produceOutputType($field->type), - 'description' => $this->getDescription($field), - 'args' => $this->makeInputValues($field->arguments), - 'deprecationReason' => $this->getDeprecationReason($field), - 'astNode' => $field - ]; - } - ); - } - - private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def) - { - if (isset($def->interfaces)) { - return Utils::map($def->interfaces, function ($iface) { - return $this->produceInterfaceType($iface); - }); - } - return null; - } - - private function makeInputValues($values) - { - return Utils::keyValMap( - $values, - function ($value) { - return $value->name->value; - }, - function ($value) { - $type = $this->produceInputType($value->type); - $config = [ - 'name' => $value->name->value, - 'type' => $type, - 'description' => $this->getDescription($value), - 'astNode' => $value - ]; - if (isset($value->defaultValue)) { - $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type); - } - return $config; - } - ); - } - - private function makeInterfaceDefConfig(InterfaceTypeDefinitionNode $def) - { - $typeName = $def->name->value; - return [ - 'name' => $typeName, - 'description' => $this->getDescription($def), - 'fields' => function () use ($def) { - return $this->makeFieldDefMap($def); - }, - 'astNode' => $def - ]; - } - - private function makeEnumDefConfig(EnumTypeDefinitionNode $def) - { - return [ - 'name' => $def->name->value, - 'description' => $this->getDescription($def), - 'astNode' => $def, - 'values' => Utils::keyValMap( - $def->values, - function ($enumValue) { - return $enumValue->name->value; - }, - function ($enumValue) { - return [ - 'description' => $this->getDescription($enumValue), - 'deprecationReason' => $this->getDeprecationReason($enumValue), - 'astNode' => $enumValue - ]; - } - ) - ]; - } - - private function makeUnionDefConfig(UnionTypeDefinitionNode $def) - { - return [ - 'name' => $def->name->value, - 'description' => $this->getDescription($def), - 'types' => Utils::map($def->types, function ($typeNode) { - return $this->produceObjectType($typeNode); - }), - 'astNode' => $def - ]; - } - - private function makeScalarDefConfig(ScalarTypeDefinitionNode $def) - { - return [ - 'name' => $def->name->value, - 'description' => $this->getDescription($def), - 'astNode' => $def, - 'serialize' => function($value) { - return $value; - }, - ]; - } - - private function makeInputObjectDefConfig(InputObjectTypeDefinitionNode $def) - { - return [ - 'name' => $def->name->value, - 'description' => $this->getDescription($def), - 'fields' => function () use ($def) { - return $this->makeInputValues($def->fields); - }, - 'astNode' => $def, - ]; - } - - /** - * Given a collection of directives, returns the string value for the - * deprecation reason. - * - * @param EnumValueDefinitionNode | FieldDefinitionNode $node - * @return string - */ - private function getDeprecationReason($node) - { - $deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node); - return isset($deprecated['reason']) ? $deprecated['reason'] : null; - } - - /** - * Given an ast node, returns its string description. - */ - public function getDescription($node) - { - if ($node->description) { - return $node->description->value; - } - if (isset($this->options['commentDescriptions'])) { - $rawValue = $this->getLeadingCommentBlock($node); - if ($rawValue !== null) { - return BlockString::value("\n" . $rawValue); - } - } - } - - public function getLeadingCommentBlock($node) - { - $loc = $node->loc; - if (!$loc || !$loc->startToken) { - return; - } - $comments = []; - $token = $loc->startToken->prev; - while ( - $token && - $token->kind === Token::COMMENT && - $token->next && $token->prev && - $token->line + 1 === $token->next->line && - $token->line !== $token->prev->line - ) { - $value = $token->value; - $comments[] = $value; - $token = $token->prev; - } - - return implode("\n", array_reverse($comments)); - } - /** * A helper function to build a GraphQLSchema directly from a source * document. * * @api * @param DocumentNode|Source|string $source - * @param callable $typeConfigDecorator + * @param array $options * @return Schema */ - public static function build($source, callable $typeConfigDecorator = null) + public static function build($source, array $options = []) { $doc = $source instanceof DocumentNode ? $source : Parser::parse($source); - return self::buildAST($doc, $typeConfigDecorator); + return self::buildAST($doc, $options); } } diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index 11054fd..b77220a 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -20,7 +20,7 @@ class BuildSchemaTest extends \PHPUnit_Framework_TestCase private function cycleOutput($body, $options = []) { $ast = Parser::parse($body); - $schema = BuildSchema::buildAST($ast, null, $options); + $schema = BuildSchema::buildAST($ast, $options); return "\n" . SchemaPrinter::doPrint($schema, $options); } @@ -1172,131 +1172,4 @@ type Repeated { $this->setExpectedException('GraphQL\Error\Error', 'Type "Repeated" was defined more than once.'); BuildSchema::buildAST($doc); } - - public function testSupportsTypeConfigDecorator() - { - $body = ' -schema { - query: Query -} - -type Query { - str: String - color: Color - hello: Hello -} - -enum Color { - RED - GREEN - BLUE -} - -interface Hello { - world: String -} -'; - $doc = Parser::parse($body); - - $decorated = []; - $calls = []; - - $typeConfigDecorator = function($defaultConfig, $node, $allNodesMap) use (&$decorated, &$calls) { - $decorated[] = $defaultConfig['name']; - $calls[] = [$defaultConfig, $node, $allNodesMap]; - return ['description' => 'My description of ' . $node->name->value] + $defaultConfig; - }; - - $schema = BuildSchema::buildAST($doc, $typeConfigDecorator); - $schema->getTypeMap(); - $this->assertEquals(['Query', 'Color', 'Hello'], $decorated); - - list($defaultConfig, $node, $allNodesMap) = $calls[0]; - $this->assertInstanceOf(ObjectTypeDefinitionNode::class, $node); - $this->assertEquals('Query', $defaultConfig['name']); - $this->assertInstanceOf(\Closure::class, $defaultConfig['fields']); - $this->assertInstanceOf(\Closure::class, $defaultConfig['interfaces']); - $this->assertArrayHasKey('description', $defaultConfig); - $this->assertCount(5, $defaultConfig); - $this->assertEquals(array_keys($allNodesMap), ['Query', 'Color', 'Hello']); - $this->assertEquals('My description of Query', $schema->getType('Query')->description); - - - list($defaultConfig, $node, $allNodesMap) = $calls[1]; - $this->assertInstanceOf(EnumTypeDefinitionNode::class, $node); - $this->assertEquals('Color', $defaultConfig['name']); - $enumValue = [ - 'description' => '', - 'deprecationReason' => '' - ]; - $this->assertArraySubset([ - 'RED' => $enumValue, - 'GREEN' => $enumValue, - 'BLUE' => $enumValue, - ], $defaultConfig['values']); - $this->assertCount(4, $defaultConfig); // 3 + astNode - $this->assertEquals(array_keys($allNodesMap), ['Query', 'Color', 'Hello']); - $this->assertEquals('My description of Color', $schema->getType('Color')->description); - - list($defaultConfig, $node, $allNodesMap) = $calls[2]; - $this->assertInstanceOf(InterfaceTypeDefinitionNode::class, $node); - $this->assertEquals('Hello', $defaultConfig['name']); - $this->assertInstanceOf(\Closure::class, $defaultConfig['fields']); - $this->assertArrayHasKey('description', $defaultConfig); - $this->assertCount(4, $defaultConfig); - $this->assertEquals(array_keys($allNodesMap), ['Query', 'Color', 'Hello']); - $this->assertEquals('My description of Hello', $schema->getType('Hello')->description); - } - - public function testCreatesTypesLazily() - { - $body = ' -schema { - query: Query -} - -type Query { - str: String - color: Color - hello: Hello -} - -enum Color { - RED - GREEN - BLUE -} - -interface Hello { - world: String -} - -type World implements Hello { - world: String -} -'; - $doc = Parser::parse($body); - $created = []; - - $typeConfigDecorator = function($config, $node) use (&$created) { - $created[] = $node->name->value; - return $config; - }; - - $schema = BuildSchema::buildAST($doc, $typeConfigDecorator); - $this->assertEquals(['Query'], $created); - - $schema->getType('Color'); - $this->assertEquals(['Query', 'Color'], $created); - - $schema->getType('Hello'); - $this->assertEquals(['Query', 'Color', 'Hello'], $created); - - $types = $schema->getTypeMap(); - $this->assertEquals(['Query', 'Color', 'Hello', 'World'], $created); - $this->assertArrayHasKey('Query', $types); - $this->assertArrayHasKey('Color', $types); - $this->assertArrayHasKey('Hello', $types); - $this->assertArrayHasKey('World', $types); - } }