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.
This commit is contained in:
Daniel Tschinder 2018-02-09 16:14:04 +01:00
parent 48c33302a8
commit 2cbccb87db
3 changed files with 461 additions and 570 deletions

View File

@ -0,0 +1,437 @@
<?php
namespace GraphQL\Utils;
use GraphQL\Error\Error;
use GraphQL\Executor\Values;
use GraphQL\Language\AST\DirectiveDefinitionNode;
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\ListTypeNode;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\TypeNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\Token;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\OutputType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
class ASTDefinitionBuilder
{
/**
* @var array
*/
private $typeDefintionsMap;
/**
* @var array
*/
private $options;
/**
* @var callable
*/
private $resolveType;
/**
* @var array
*/
private $cache;
public function __construct(array $typeDefintionsMap, $options, callable $resolveType)
{
$this->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));
}
}

View File

@ -2,35 +2,13 @@
namespace GraphQL\Utils; namespace GraphQL\Utils;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Executor\Values;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\DocumentNode; 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\NodeKind;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\SchemaDefinitionNode; use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\AST\TypeNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Language\Source; use GraphQL\Language\Source;
use GraphQL\Language\Token;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use GraphQL\Type\Definition\Directive; 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) * 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 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 * This takes the ast of a schema document produced by the parse function in
* GraphQL\Language\Parser. * GraphQL\Language\Parser.
@ -75,7 +26,7 @@ class BuildSchema
* Given that AST it constructs a GraphQL\Type\Schema. The resulting schema * Given that AST it constructs a GraphQL\Type\Schema. The resulting schema
* has no resolve methods, so execution will use default resolvers. * has no resolve methods, so execution will use default resolvers.
* *
* Accepts options as a third argument: * Accepts options as a second argument:
* *
* - commentDescriptions: * - commentDescriptions:
* Provide true to use preceding comments as the description. * Provide true to use preceding comments as the description.
@ -83,27 +34,24 @@ class BuildSchema
* *
* @api * @api
* @param DocumentNode $ast * @param DocumentNode $ast
* @param callable $typeConfigDecorator * @param array $options
* @return Schema * @return Schema
* @throws Error * @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(); return $builder->buildSchema();
} }
private $ast; private $ast;
private $innerTypeMap;
private $nodeMap; private $nodeMap;
private $typeConfigDecorator;
private $loadedTypeDefs; private $loadedTypeDefs;
private $options; private $options;
public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = []) public function __construct(DocumentNode $ast, array $options = [])
{ {
$this->ast = $ast; $this->ast = $ast;
$this->typeConfigDecorator = $typeConfigDecorator;
$this->loadedTypeDefs = []; $this->loadedTypeDefs = [];
$this->options = $options; $this->options = $options;
} }
@ -150,23 +98,15 @@ class BuildSchema
'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null, 'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null,
]; ];
$this->innerTypeMap = [ $defintionBuilder = new ASTDefinitionBuilder(
'String' => Type::string(), $this->nodeMap,
'Int' => Type::int(), $this->options,
'Float' => Type::float(), function($typeName) { throw new Error('Type "'. $typeName . '" not found in document.'); }
'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(),
];
$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. // If specified directives were not explicitly declared, add them.
$skip = array_reduce($directives, function ($hasSkip, $directive) { $skip = array_reduce($directives, function ($hasSkip, $directive) {
@ -197,23 +137,23 @@ class BuildSchema
} }
$schema = new Schema([ $schema = new Schema([
'query' => $this->getObjectType($operationTypes['query']), 'query' => $defintionBuilder->buildObjectType($operationTypes['query']),
'mutation' => isset($operationTypes['mutation']) ? 'mutation' => isset($operationTypes['mutation']) ?
$this->getObjectType($operationTypes['mutation']) : $defintionBuilder->buildObjectType($operationTypes['mutation']) :
null, null,
'subscription' => isset($operationTypes['subscription']) ? 'subscription' => isset($operationTypes['subscription']) ?
$this->getObjectType($operationTypes['subscription']) : $defintionBuilder->buildObjectType($operationTypes['subscription']) :
null, null,
'typeLoader' => function ($name) { 'typeLoader' => function ($name) use ($defintionBuilder) {
return $this->typeDefNamed($name); return $defintionBuilder->buildType($name);
}, },
'directives' => $directives, 'directives' => $directives,
'astNode' => $schemaDef, 'astNode' => $schemaDef,
'types' => function () { 'types' => function () use ($defintionBuilder) {
$types = []; $types = [];
foreach ($this->nodeMap as $name => $def) { foreach ($this->nodeMap as $name => $def) {
if (!isset($this->loadedTypeDefs[$name])) { if (!isset($this->loadedTypeDefs[$name])) {
$types[] = $this->typeDefNamed($def->name->value); $types[] = $defintionBuilder->buildType($def->name->value);
} }
} }
return $types; return $types;
@ -250,377 +190,18 @@ class BuildSchema
return $opTypes; 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 * A helper function to build a GraphQLSchema directly from a source
* document. * document.
* *
* @api * @api
* @param DocumentNode|Source|string $source * @param DocumentNode|Source|string $source
* @param callable $typeConfigDecorator * @param array $options
* @return Schema * @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); $doc = $source instanceof DocumentNode ? $source : Parser::parse($source);
return self::buildAST($doc, $typeConfigDecorator); return self::buildAST($doc, $options);
} }
} }

View File

@ -20,7 +20,7 @@ class BuildSchemaTest extends \PHPUnit_Framework_TestCase
private function cycleOutput($body, $options = []) private function cycleOutput($body, $options = [])
{ {
$ast = Parser::parse($body); $ast = Parser::parse($body);
$schema = BuildSchema::buildAST($ast, null, $options); $schema = BuildSchema::buildAST($ast, $options);
return "\n" . SchemaPrinter::doPrint($schema, $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.'); $this->setExpectedException('GraphQL\Error\Error', 'Type "Repeated" was defined more than once.');
BuildSchema::buildAST($doc); 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);
}
} }