mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-29 08:26:02 +03:00
Merge pull request #91 from petecoop/build-schema
Build Schema & Schema Printer
This commit is contained in:
commit
ed41a4ce43
@ -94,7 +94,7 @@ class Values
|
||||
* @return array
|
||||
* @throws Error
|
||||
*/
|
||||
public static function getArgumentValues($def, $node, $variableValues)
|
||||
public static function getArgumentValues($def, $node, $variableValues = null)
|
||||
{
|
||||
$argDefs = $def->args;
|
||||
$argNodes = $node->arguments;
|
||||
|
@ -107,7 +107,7 @@ class Directive
|
||||
new FieldArgument([
|
||||
'name' => 'if',
|
||||
'type' => Type::nonNull(Type::boolean()),
|
||||
'description' => 'Skipped when true'
|
||||
'description' => 'Skipped when true.'
|
||||
])
|
||||
]
|
||||
]),
|
||||
|
@ -235,8 +235,8 @@ EOD;
|
||||
'description' =>
|
||||
'A GraphQL Schema defines the capabilities of a GraphQL ' .
|
||||
'server. It exposes all available types and directives on ' .
|
||||
'the server, as well as the entry points for query and ' .
|
||||
'mutation operations.',
|
||||
'the server, as well as the entry points for query, mutation, and ' .
|
||||
'subscription operations.',
|
||||
'fields' => [
|
||||
'types' => [
|
||||
'description' => 'A list of all types supported by this server.',
|
||||
@ -288,7 +288,7 @@ EOD;
|
||||
'name' => '__Directive',
|
||||
'description' => 'A Directive provides a way to describe alternate runtime execution and ' .
|
||||
'type validation behavior in a GraphQL document.' .
|
||||
'\n\nIn some cases, you need to provide options to alter GraphQL’s ' .
|
||||
"\n\nIn some cases, you need to provide options to alter GraphQL's " .
|
||||
'execution behavior in ways field arguments will not suffice, such as ' .
|
||||
'conditionally including or skipping a field. Directives provide this by ' .
|
||||
'describing additional information to the executor.',
|
||||
@ -664,7 +664,7 @@ EOD;
|
||||
if (!isset(self::$map['__TypeKind'])) {
|
||||
self::$map['__TypeKind'] = new EnumType([
|
||||
'name' => '__TypeKind',
|
||||
'description' => 'An enum describing what kind of type a given __Type is.',
|
||||
'description' => 'An enum describing what kind of type a given `__Type` is.',
|
||||
'values' => [
|
||||
'SCALAR' => [
|
||||
'value' => TypeKind::SCALAR,
|
||||
|
@ -180,6 +180,14 @@ class Utils
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
public static function keyValMap($traversable, callable $keyFn, callable $valFn)
|
||||
{
|
||||
return array_reduce($traversable, function ($map, $item) use ($keyFn, $valFn) {
|
||||
$map[$keyFn($item)] = $valFn($item);
|
||||
return $map;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $traversable
|
||||
* @param callable $predicate
|
||||
|
530
src/Utils/BuildSchema.php
Normal file
530
src/Utils/BuildSchema.php
Normal file
@ -0,0 +1,530 @@
|
||||
<?php
|
||||
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\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\TypeDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeNode;
|
||||
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Language\Token;
|
||||
use GraphQL\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;
|
||||
use GraphQL\Utils;
|
||||
|
||||
/**
|
||||
* Class BuildSchema
|
||||
* @package GraphQL\Utils
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* If no schema definition is provided, then it will look for types named Query
|
||||
* and Mutation.
|
||||
*
|
||||
* Given that AST it constructs a GraphQLSchema. The resulting schema
|
||||
* has no resolve methods, so execution will use default resolvers.
|
||||
*
|
||||
* @param DocumentNode $ast
|
||||
* @return Schema
|
||||
* @throws Error
|
||||
*/
|
||||
public static function buildAST(DocumentNode $ast)
|
||||
{
|
||||
$builder = new self($ast);
|
||||
return $builder->buildSchema();
|
||||
}
|
||||
|
||||
private $ast;
|
||||
private $innerTypeMap;
|
||||
private $nodeMap;
|
||||
|
||||
public function __construct(DocumentNode $ast)
|
||||
{
|
||||
$this->ast = $ast;
|
||||
}
|
||||
|
||||
public function buildSchema()
|
||||
{
|
||||
$schemaDef = null;
|
||||
$typeDefs = [];
|
||||
$this->nodeMap = [];
|
||||
$directiveDefs = [];
|
||||
foreach ($this->ast->definitions as $d) {
|
||||
switch ($d->kind) {
|
||||
case NodeKind::SCHEMA_DEFINITION:
|
||||
if ($schemaDef) {
|
||||
throw new Error('Must provide only one schema definition.');
|
||||
}
|
||||
$schemaDef = $d;
|
||||
break;
|
||||
case NodeKind::SCALAR_TYPE_DEFINITION:
|
||||
case NodeKind::OBJECT_TYPE_DEFINITION:
|
||||
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
||||
case NodeKind::ENUM_TYPE_DEFINITION:
|
||||
case NodeKind::UNION_TYPE_DEFINITION:
|
||||
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
||||
$typeDefs[] = $d;
|
||||
$this->nodeMap[$d->name->value] = $d;
|
||||
break;
|
||||
case NodeKind::DIRECTIVE_DEFINITION:
|
||||
$directiveDefs[] = $d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$queryTypeName = null;
|
||||
$mutationTypeName = null;
|
||||
$subscriptionTypeName = null;
|
||||
if ($schemaDef) {
|
||||
foreach ($schemaDef->operationTypes as $operationType) {
|
||||
$typeName = $operationType->type->name->value;
|
||||
if ($operationType->operation === 'query') {
|
||||
if ($queryTypeName) {
|
||||
throw new Error('Must provide only one query type in schema.');
|
||||
}
|
||||
if (!isset($this->nodeMap[$typeName])) {
|
||||
throw new Error(
|
||||
'Specified query type "' . $typeName . '" not found in document.'
|
||||
);
|
||||
}
|
||||
$queryTypeName = $typeName;
|
||||
} else if ($operationType->operation === 'mutation') {
|
||||
if ($mutationTypeName) {
|
||||
throw new Error('Must provide only one mutation type in schema.');
|
||||
}
|
||||
if (!isset($this->nodeMap[$typeName])) {
|
||||
throw new Error(
|
||||
'Specified mutation type "' . $typeName . '" not found in document.'
|
||||
);
|
||||
}
|
||||
$mutationTypeName = $typeName;
|
||||
} else if ($operationType->operation === 'subscription') {
|
||||
if ($subscriptionTypeName) {
|
||||
throw new Error('Must provide only one subscription type in schema.');
|
||||
}
|
||||
if (!isset($this->nodeMap[$typeName])) {
|
||||
throw new Error(
|
||||
'Specified subscription type "' . $typeName . '" not found in document.'
|
||||
);
|
||||
}
|
||||
$subscriptionTypeName = $typeName;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isset($this->nodeMap['Query'])) {
|
||||
$queryTypeName = 'Query';
|
||||
}
|
||||
if (isset($this->nodeMap['Mutation'])) {
|
||||
$mutationTypeName = 'Mutation';
|
||||
}
|
||||
if (isset($this->nodeMap['Subscription'])) {
|
||||
$subscriptionTypeName = 'Subscription';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$queryTypeName) {
|
||||
throw new Error(
|
||||
'Must provide schema definition with query type or a type named Query.'
|
||||
);
|
||||
}
|
||||
|
||||
$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(),
|
||||
];
|
||||
|
||||
$types = array_map(function($def) {
|
||||
return $this->typeDefNamed($def->name->value);
|
||||
}, $typeDefs);
|
||||
|
||||
$directives = array_map([$this, 'getDirective'], $directiveDefs);
|
||||
|
||||
// If specified directives were not explicitly declared, add them.
|
||||
$skip = array_reduce($directives, function($hasSkip, $directive) {
|
||||
return $hasSkip || $directive->name == 'skip';
|
||||
});
|
||||
if (!$skip) {
|
||||
$directives[] = Directive::skipDirective();
|
||||
}
|
||||
|
||||
$include = array_reduce($directives, function($hasInclude, $directive) {
|
||||
return $hasInclude || $directive->name == 'include';
|
||||
});
|
||||
if (!$include) {
|
||||
$directives[] = Directive::includeDirective();
|
||||
}
|
||||
|
||||
$deprecated = array_reduce($directives, function($hasDeprecated, $directive) {
|
||||
return $hasDeprecated || $directive->name == 'deprecated';
|
||||
});
|
||||
if (!$deprecated) {
|
||||
$directives[] = Directive::deprecatedDirective();
|
||||
}
|
||||
|
||||
return new Schema([
|
||||
'query' => $this->getObjectType($this->nodeMap[$queryTypeName]),
|
||||
'mutation' => $mutationTypeName ?
|
||||
$this->getObjectType($this->nodeMap[$mutationTypeName]) :
|
||||
null,
|
||||
'subscription' => $subscriptionTypeName ?
|
||||
$this->getObjectType($this->nodeMap[$subscriptionTypeName]) :
|
||||
null,
|
||||
'types' => $types,
|
||||
'directives' => $directives,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getDirective(DirectiveDefinitionNode $directiveNode)
|
||||
{
|
||||
return new Directive([
|
||||
'name' => $directiveNode->name->value,
|
||||
'description' => $this->getDescription($directiveNode),
|
||||
'locations' => array_map(function($node) {
|
||||
return $node->value;
|
||||
}, $directiveNode->locations),
|
||||
'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getObjectType(TypeDefinitionNode $typeNode)
|
||||
{
|
||||
$type = $this->typeDefNamed($typeNode->name->value);
|
||||
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 Input 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.');
|
||||
}
|
||||
|
||||
$innerTypeDef = $this->makeSchemaDef($this->nodeMap[$typeName]);
|
||||
if (!$innerTypeDef) {
|
||||
throw new Error("Nothing constructed for $typeName.");
|
||||
}
|
||||
$this->innerTypeMap[$typeName] = $innerTypeDef;
|
||||
return $innerTypeDef;
|
||||
}
|
||||
|
||||
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); }
|
||||
]);
|
||||
}
|
||||
|
||||
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->directives)
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
|
||||
{
|
||||
return isset($def->interfaces) ? array_map([$this, 'produceInterfaceType'], $def->interfaces) : 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)
|
||||
];
|
||||
if (isset($value->defaultValue)) {
|
||||
$config['defaultValue'] = Utils\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); },
|
||||
'resolveType' => [$this, 'cannotExecuteSchema']
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeEnumDef(EnumTypeDefinitionNode $def)
|
||||
{
|
||||
return new EnumType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'values' => Utils::keyValMap(
|
||||
$def->values,
|
||||
function($enumValue) {
|
||||
return $enumValue->name->value;
|
||||
},
|
||||
function($enumValue) {
|
||||
return [
|
||||
'description' => $this->getDescription($enumValue),
|
||||
'deprecationReason' => $this->getDeprecationReason($enumValue->directives)
|
||||
];
|
||||
}
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeUnionDef(UnionTypeDefinitionNode $def)
|
||||
{
|
||||
return new UnionType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'types' => array_map([$this, 'produceObjectType'], $def->types),
|
||||
'resolveType' => [$this, 'cannotExecuteSchema']
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeScalarDef(ScalarTypeDefinitionNode $def)
|
||||
{
|
||||
return new CustomScalarType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'serialize' => function() { return false; },
|
||||
// Note: validation calls the parse functions to determine if a
|
||||
// literal value is correct. Returning null would cause use of custom
|
||||
// scalars to always fail validation. Returning false causes them to
|
||||
// always pass validation.
|
||||
'parseValue' => function() { return false; },
|
||||
'parseLiteral' => function() { return false; }
|
||||
]);
|
||||
}
|
||||
|
||||
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); }
|
||||
]);
|
||||
}
|
||||
|
||||
private function getDeprecationReason($directives)
|
||||
{
|
||||
$deprecatedAST = $directives ? Utils::find(
|
||||
$directives,
|
||||
function($directive) {
|
||||
return $directive->name->value === Directive::deprecatedDirective()->name;
|
||||
}
|
||||
) : null;
|
||||
if (!$deprecatedAST) {
|
||||
return;
|
||||
}
|
||||
return Values::getArgumentValues(
|
||||
Directive::deprecatedDirective(),
|
||||
$deprecatedAST
|
||||
)['reason'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an ast node, returns its string description based on a contiguous
|
||||
* block full-line of comments preceding it.
|
||||
*/
|
||||
public function getDescription($node)
|
||||
{
|
||||
$loc = $node->loc;
|
||||
if (!$loc) {
|
||||
return;
|
||||
}
|
||||
$comments = [];
|
||||
$minSpaces = null;
|
||||
$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;
|
||||
$spaces = $this->leadingSpaces($value);
|
||||
if ($minSpaces === null || $spaces < $minSpaces) {
|
||||
$minSpaces = $spaces;
|
||||
}
|
||||
$comments[] = $value;
|
||||
$token = $token->prev;
|
||||
}
|
||||
return implode("\n", array_map(function($comment) use ($minSpaces) {
|
||||
return mb_substr(str_replace("\n", '', $comment), $minSpaces);
|
||||
}, array_reverse($comments)));
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to build a GraphQLSchema directly from a source
|
||||
* document.
|
||||
*
|
||||
* @param Source|string $source
|
||||
* @return
|
||||
*/
|
||||
public static function build($source)
|
||||
{
|
||||
return self::buildAST(Parser::parse($source));
|
||||
}
|
||||
|
||||
// Count the number of spaces on the starting side of a string.
|
||||
private function leadingSpaces($str)
|
||||
{
|
||||
return strlen($str) - strlen(ltrim($str));
|
||||
}
|
||||
|
||||
public function cannotExecuteSchema() {
|
||||
throw new Error(
|
||||
'Generated Schema cannot use Interface or Union types for execution.'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
304
src/Utils/SchemaPrinter.php
Normal file
304
src/Utils/SchemaPrinter.php
Normal file
@ -0,0 +1,304 @@
|
||||
<?php
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Utils;
|
||||
|
||||
/**
|
||||
* Class SchemaPrinter
|
||||
* @package GraphQL\Utils
|
||||
*/
|
||||
class SchemaPrinter
|
||||
{
|
||||
public static function doPrint(Schema $schema)
|
||||
{
|
||||
return self::printFilteredSchema($schema, function($n) {
|
||||
return !self::isSpecDirective($n);
|
||||
}, 'self::isDefinedType');
|
||||
}
|
||||
|
||||
public static function printIntrosepctionSchema(Schema $schema)
|
||||
{
|
||||
return self::printFilteredSchema($schema, [__CLASS__, 'isSpecDirective'], [__CLASS__, 'isIntrospectionType']);
|
||||
}
|
||||
|
||||
private static function isSpecDirective($directiveName)
|
||||
{
|
||||
return (
|
||||
$directiveName === 'skip' ||
|
||||
$directiveName === 'include' ||
|
||||
$directiveName === 'deprecated'
|
||||
);
|
||||
}
|
||||
|
||||
private static function isDefinedType($typename)
|
||||
{
|
||||
return !self::isIntrospectionType($typename) && !self::isBuiltInScalar($typename);
|
||||
}
|
||||
|
||||
private static function isIntrospectionType($typename)
|
||||
{
|
||||
return strpos($typename, '__') === 0;
|
||||
}
|
||||
|
||||
private static function isBuiltInScalar($typename)
|
||||
{
|
||||
return (
|
||||
$typename === Type::STRING ||
|
||||
$typename === Type::BOOLEAN ||
|
||||
$typename === Type::INT ||
|
||||
$typename === Type::FLOAT ||
|
||||
$typename === Type::ID
|
||||
);
|
||||
}
|
||||
|
||||
private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter)
|
||||
{
|
||||
$directives = array_filter($schema->getDirectives(), function($directive) use ($directiveFilter) {
|
||||
return $directiveFilter($directive->name);
|
||||
});
|
||||
$typeMap = $schema->getTypeMap();
|
||||
$types = array_filter(array_keys($typeMap), $typeFilter);
|
||||
sort($types);
|
||||
$types = array_map(function($typeName) use ($typeMap) { return $typeMap[$typeName]; }, $types);
|
||||
|
||||
return implode("\n\n", array_filter(array_merge(
|
||||
[self::printSchemaDefinition($schema)],
|
||||
array_map('self::printDirective', $directives),
|
||||
array_map('self::printType', $types)
|
||||
))) . "\n";
|
||||
}
|
||||
|
||||
private static function printSchemaDefinition(Schema $schema)
|
||||
{
|
||||
if (self::isSchemaOfCommonNames($schema)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$operationTypes = [];
|
||||
|
||||
$queryType = $schema->getQueryType();
|
||||
if ($queryType) {
|
||||
$operationTypes[] = " query: {$queryType->name}";
|
||||
}
|
||||
|
||||
$mutationType = $schema->getMutationType();
|
||||
if ($mutationType) {
|
||||
$operationTypes[] = " mutation: {$mutationType->name}";
|
||||
}
|
||||
|
||||
$subscriptionType = $schema->getSubscriptionType();
|
||||
if ($subscriptionType) {
|
||||
$operationTypes[] = " subscription: {$subscriptionType->name}";
|
||||
}
|
||||
|
||||
return "schema {\n" . implode("\n", $operationTypes) . "\n}";
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL schema define root types for each type of operation. These types are
|
||||
* the same as any other type and can be named in any manner, however there is
|
||||
* a common naming convention:
|
||||
*
|
||||
* schema {
|
||||
* query: Query
|
||||
* mutation: Mutation
|
||||
* }
|
||||
*
|
||||
* When using this naming convention, the schema description can be omitted.
|
||||
*/
|
||||
private static function isSchemaOfCommonNames(Schema $schema)
|
||||
{
|
||||
$queryType = $schema->getQueryType();
|
||||
if ($queryType && $queryType->name !== 'Query') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mutationType = $schema->getMutationType();
|
||||
if ($mutationType && $mutationType->name !== 'Mutation') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subscriptionType = $schema->getSubscriptionType();
|
||||
if ($subscriptionType && $subscriptionType->name !== 'Subscription') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function printType(Type $type)
|
||||
{
|
||||
if ($type instanceof ScalarType) {
|
||||
return self::printScalar($type);
|
||||
} else if ($type instanceof ObjectType) {
|
||||
return self::printObject($type);
|
||||
} else if ($type instanceof InterfaceType) {
|
||||
return self::printInterface($type);
|
||||
} else if ($type instanceof UnionType) {
|
||||
return self::printUnion($type);
|
||||
} else if ($type instanceof EnumType) {
|
||||
return self::printEnum($type);
|
||||
}
|
||||
Utils::invariant($type instanceof InputObjectType);
|
||||
return self::printInputObject($type);
|
||||
}
|
||||
|
||||
private static function printScalar(ScalarType $type)
|
||||
{
|
||||
return self::printDescription($type) . "scalar {$type->name}";
|
||||
}
|
||||
|
||||
private static function printObject(ObjectType $type)
|
||||
{
|
||||
$interfaces = $type->getInterfaces();
|
||||
$implementedInterfaces = !empty($interfaces) ?
|
||||
' implements ' . implode(', ', array_map(function($i) {
|
||||
return $i->name;
|
||||
}, $interfaces)) : '';
|
||||
return self::printDescription($type) .
|
||||
"type {$type->name}$implementedInterfaces {\n" .
|
||||
self::printFields($type) . "\n" .
|
||||
"}";
|
||||
}
|
||||
|
||||
private static function printInterface(InterfaceType $type)
|
||||
{
|
||||
return self::printDescription($type) .
|
||||
"interface {$type->name} {\n" .
|
||||
self::printFields($type) . "\n" .
|
||||
"}";
|
||||
}
|
||||
|
||||
private static function printUnion(UnionType $type)
|
||||
{
|
||||
return self::printDescription($type) .
|
||||
"union {$type->name} = " . implode(" | ", $type->getTypes());
|
||||
}
|
||||
|
||||
private static function printEnum(EnumType $type)
|
||||
{
|
||||
return self::printDescription($type) .
|
||||
"enum {$type->name} {\n" .
|
||||
self::printEnumValues($type->getValues()) . "\n" .
|
||||
"}";
|
||||
}
|
||||
|
||||
private static function printEnumValues($values)
|
||||
{
|
||||
return implode("\n", array_map(function($value, $i) {
|
||||
return self::printDescription($value, ' ', !$i) . ' ' .
|
||||
$value->name . self::printDeprecated($value);
|
||||
}, $values, array_keys($values)));
|
||||
}
|
||||
|
||||
private static function printInputObject(InputObjectType $type)
|
||||
{
|
||||
$fields = array_values($type->getFields());
|
||||
return self::printDescription($type) .
|
||||
"input {$type->name} {\n" .
|
||||
implode("\n", array_map(function($f, $i) {
|
||||
return self::printDescription($f, ' ', !$i) . ' ' . self::printInputValue($f);
|
||||
}, $fields, array_keys($fields))) . "\n" .
|
||||
"}";
|
||||
}
|
||||
|
||||
private static function printFields($type)
|
||||
{
|
||||
$fields = array_values($type->getFields());
|
||||
return implode("\n", array_map(function($f, $i) {
|
||||
return self::printDescription($f, ' ', !$i) . ' ' .
|
||||
$f->name . self::printArgs($f->args, ' ') . ': ' .
|
||||
(string) $f->getType() . self::printDeprecated($f);
|
||||
}, $fields, array_keys($fields)));
|
||||
}
|
||||
|
||||
private static function printArgs($args, $indentation = '')
|
||||
{
|
||||
if (count($args) === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// If every arg does not have a description, print them on one line.
|
||||
if (Utils::every($args, function($arg) { return empty($arg->description); })) {
|
||||
return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')';
|
||||
}
|
||||
|
||||
return "(\n" . implode("\n", array_map(function($arg, $i) use ($indentation) {
|
||||
return self::printDescription($arg, ' ' . $indentation, !$i) . ' ' . $indentation .
|
||||
self::printInputValue($arg);
|
||||
}, $args, array_keys($args))) . "\n" . $indentation . ')';
|
||||
}
|
||||
|
||||
private static function printInputValue($arg)
|
||||
{
|
||||
$argDecl = $arg->name . ': ' . (string) $arg->getType();
|
||||
if ($arg->defaultValueExists()) {
|
||||
$argDecl .= ' = ' . Printer::doPrint(AST::astFromValue($arg->defaultValue, $arg->getType()));
|
||||
}
|
||||
return $argDecl;
|
||||
}
|
||||
|
||||
private static function printDirective($directive)
|
||||
{
|
||||
return self::printDescription($directive) .
|
||||
'directive @' . $directive->name . self::printArgs($directive->args) .
|
||||
' on ' . implode(' | ', $directive->locations);
|
||||
}
|
||||
|
||||
private static function printDeprecated($fieldOrEnumVal)
|
||||
{
|
||||
$reason = $fieldOrEnumVal->deprecationReason;
|
||||
if (empty($reason)) {
|
||||
return '';
|
||||
}
|
||||
if ($reason === '' || $reason === Directive::DEFAULT_DEPRECATION_REASON) {
|
||||
return ' @deprecated';
|
||||
}
|
||||
return ' @deprecated(reason: ' .
|
||||
Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')';
|
||||
}
|
||||
|
||||
private static function printDescription($def, $indentation = '', $firstInBlock = true)
|
||||
{
|
||||
if (!$def->description) {
|
||||
return '';
|
||||
}
|
||||
$lines = explode("\n", $def->description);
|
||||
$description = $indentation && !$firstInBlock ? "\n" : '';
|
||||
foreach ($lines as $line) {
|
||||
if ($line === '') {
|
||||
$description .= $indentation . "#\n";
|
||||
} else {
|
||||
// For > 120 character long lines, cut at space boundaries into sublines
|
||||
// of ~80 chars.
|
||||
$sublines = self::breakLine($line, 120 - strlen($indentation));
|
||||
foreach ($sublines as $subline) {
|
||||
$description .= $indentation . '# ' . $subline . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
return $description;
|
||||
}
|
||||
|
||||
private static function breakLine($line, $len)
|
||||
{
|
||||
if (strlen($line) < $len + 5) {
|
||||
return [$line];
|
||||
}
|
||||
preg_match_all("/((?: |^).{15," . ($len - 40) . "}(?= |$))/", $line, $parts);
|
||||
$parts = $parts[0];
|
||||
return array_map(function($part) {
|
||||
return trim($part);
|
||||
}, $parts);
|
||||
}
|
||||
}
|
@ -1514,7 +1514,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
'description' => 'A GraphQL Schema defines the capabilities of a ' .
|
||||
'GraphQL server. It exposes all available types and ' .
|
||||
'directives on the server, as well as the entry ' .
|
||||
'points for query and mutation operations.',
|
||||
'points for query, mutation, and subscription operations.',
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'types',
|
||||
@ -1571,7 +1571,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
'data' => [
|
||||
'typeKindType' => [
|
||||
'name' => '__TypeKind',
|
||||
'description' => 'An enum describing what kind of type a given __Type is.',
|
||||
'description' => 'An enum describing what kind of type a given `__Type` is.',
|
||||
'enumValues' => [
|
||||
[
|
||||
'description' => 'Indicates this type is a scalar.',
|
||||
|
899
tests/Utils/BuildSchemaTest.php
Normal file
899
tests/Utils/BuildSchemaTest.php
Normal file
@ -0,0 +1,899 @@
|
||||
<?php
|
||||
namespace GraphQL\Tests\Utils;
|
||||
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Utils\BuildSchema;
|
||||
use GraphQL\Utils\SchemaPrinter;
|
||||
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumValueDefinition;
|
||||
|
||||
class BuildSchemaTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// Describe: Schema Builder
|
||||
|
||||
private function cycleOutput($body)
|
||||
{
|
||||
$ast = Parser::parse($body);
|
||||
$schema = BuildSchema::buildAST($ast);
|
||||
return "\n" . SchemaPrinter::doPrint($schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it can use built schema for limited execution
|
||||
*/
|
||||
public function testUseBuiltSchemaForLimitedExecution()
|
||||
{
|
||||
$schema = BuildSchema::buildAST(Parser::parse('
|
||||
schema { query: Query }
|
||||
type Query {
|
||||
str: String
|
||||
}
|
||||
'));
|
||||
|
||||
$result = GraphQL::execute($schema, '{ str }', ['str' => 123]);
|
||||
$this->assertEquals($result['data'], ['str' => 123]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it can build a schema directly from the source
|
||||
*/
|
||||
public function testBuildSchemaDirectlyFromSource()
|
||||
{
|
||||
$schema = BuildSchema::build("
|
||||
schema { query: Query }
|
||||
type Query {
|
||||
add(x: Int, y: Int): Int
|
||||
}
|
||||
");
|
||||
|
||||
$result = GraphQL::execute(
|
||||
$schema,
|
||||
'{ add(x: 34, y: 55) }',
|
||||
[
|
||||
'add' => function ($root, $args) {
|
||||
return $args['x'] + $args['y'];
|
||||
}
|
||||
]
|
||||
);
|
||||
$this->assertEquals($result, ['data' => ['add' => 89]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Simple Type
|
||||
*/
|
||||
public function testSimpleType()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: HelloScalars
|
||||
}
|
||||
|
||||
type HelloScalars {
|
||||
str: String
|
||||
int: Int
|
||||
float: Float
|
||||
id: ID
|
||||
bool: Boolean
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it With directives
|
||||
*/
|
||||
public function testWithDirectives()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
directive @foo(arg: Int) on FIELD
|
||||
|
||||
type Hello {
|
||||
str: String
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Supports descriptions
|
||||
*/
|
||||
public function testSupportsDescriptions()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
# This is a directive
|
||||
directive @foo(
|
||||
# It has an argument
|
||||
arg: Int
|
||||
) on FIELD
|
||||
|
||||
# With an enum
|
||||
enum Color {
|
||||
RED
|
||||
|
||||
# Not a creative color
|
||||
GREEN
|
||||
BLUE
|
||||
}
|
||||
|
||||
# What a great type
|
||||
type Hello {
|
||||
# And a field to boot
|
||||
str: String
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Maintains @skip & @include
|
||||
*/
|
||||
public function testMaintainsSkipAndInclude()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
type Hello {
|
||||
str: String
|
||||
}
|
||||
';
|
||||
$schema = BuildSchema::buildAST(Parser::parse($body));
|
||||
$this->assertEquals(count($schema->getDirectives()), 3);
|
||||
$this->assertEquals($schema->getDirective('skip'), Directive::skipDirective());
|
||||
$this->assertEquals($schema->getDirective('include'), Directive::includeDirective());
|
||||
$this->assertEquals($schema->getDirective('deprecated'), Directive::deprecatedDirective());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Overriding directives excludes specified
|
||||
*/
|
||||
public function testOverridingDirectivesExcludesSpecified()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
directive @skip on FIELD
|
||||
directive @include on FIELD
|
||||
directive @deprecated on FIELD_DEFINITION
|
||||
|
||||
type Hello {
|
||||
str: String
|
||||
}
|
||||
';
|
||||
$schema = BuildSchema::buildAST(Parser::parse($body));
|
||||
$this->assertEquals(count($schema->getDirectives()), 3);
|
||||
$this->assertNotEquals($schema->getDirective('skip'), Directive::skipDirective());
|
||||
$this->assertNotEquals($schema->getDirective('include'), Directive::includeDirective());
|
||||
$this->assertNotEquals($schema->getDirective('deprecated'), Directive::deprecatedDirective());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Type modifiers
|
||||
*/
|
||||
public function testTypeModifiers()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: HelloScalars
|
||||
}
|
||||
|
||||
type HelloScalars {
|
||||
nonNullStr: String!
|
||||
listOfStrs: [String]
|
||||
listOfNonNullStrs: [String!]
|
||||
nonNullListOfStrs: [String]!
|
||||
nonNullListOfNonNullStrs: [String!]!
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Recursive type
|
||||
*/
|
||||
public function testRecursiveType()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Recurse
|
||||
}
|
||||
|
||||
type Recurse {
|
||||
str: String
|
||||
recurse: Recurse
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Two types circular
|
||||
*/
|
||||
public function testTwoTypesCircular()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: TypeOne
|
||||
}
|
||||
|
||||
type TypeOne {
|
||||
str: String
|
||||
typeTwo: TypeTwo
|
||||
}
|
||||
|
||||
type TypeTwo {
|
||||
str: String
|
||||
typeOne: TypeOne
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Single argument field
|
||||
*/
|
||||
public function testSingleArgumentField()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
type Hello {
|
||||
str(int: Int): String
|
||||
floatToStr(float: Float): String
|
||||
idToStr(id: ID): String
|
||||
booleanToStr(bool: Boolean): String
|
||||
strToStr(bool: String): String
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Simple type with multiple arguments
|
||||
*/
|
||||
public function testSimpleTypeWithMultipleArguments()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
type Hello {
|
||||
str(int: Int, bool: Boolean): String
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Simple type with interface
|
||||
*/
|
||||
public function testSimpleTypeWithInterface()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
type Hello implements WorldInterface {
|
||||
str: String
|
||||
}
|
||||
|
||||
interface WorldInterface {
|
||||
str: String
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Simple output enum
|
||||
*/
|
||||
public function testSimpleOutputEnum()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: OutputEnumRoot
|
||||
}
|
||||
|
||||
enum Hello {
|
||||
WORLD
|
||||
}
|
||||
|
||||
type OutputEnumRoot {
|
||||
hello: Hello
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Multiple value enum
|
||||
*/
|
||||
public function testMultipleValueEnum()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: OutputEnumRoot
|
||||
}
|
||||
|
||||
enum Hello {
|
||||
WO
|
||||
RLD
|
||||
}
|
||||
|
||||
type OutputEnumRoot {
|
||||
hello: Hello
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Simple Union
|
||||
*/
|
||||
public function testSimpleUnion()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
union Hello = World
|
||||
|
||||
type Root {
|
||||
hello: Hello
|
||||
}
|
||||
|
||||
type World {
|
||||
str: String
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Multiple Union
|
||||
*/
|
||||
public function testMultipleUnion()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
union Hello = WorldOne | WorldTwo
|
||||
|
||||
type Root {
|
||||
hello: Hello
|
||||
}
|
||||
|
||||
type WorldOne {
|
||||
str: String
|
||||
}
|
||||
|
||||
type WorldTwo {
|
||||
str: String
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it CustomScalar
|
||||
*/
|
||||
public function testCustomScalar()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
scalar CustomScalar
|
||||
|
||||
type Root {
|
||||
customScalar: CustomScalar
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it CustomScalar
|
||||
*/
|
||||
public function testInputObject()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
input Input {
|
||||
int: Int
|
||||
}
|
||||
|
||||
type Root {
|
||||
field(in: Input): String
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Simple argument field with default
|
||||
*/
|
||||
public function testSimpleArgumentFieldWithDefault()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
type Hello {
|
||||
str(int: Int = 2): String
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Simple type with mutation
|
||||
*/
|
||||
public function testSimpleTypeWithMutation()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: HelloScalars
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
type HelloScalars {
|
||||
str: String
|
||||
int: Int
|
||||
bool: Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
addHelloScalars(str: String, int: Int, bool: Boolean): HelloScalars
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Simple type with subscription
|
||||
*/
|
||||
public function testSimpleTypeWithSubscription()
|
||||
{
|
||||
$body = '
|
||||
schema {
|
||||
query: HelloScalars
|
||||
subscription: Subscription
|
||||
}
|
||||
|
||||
type HelloScalars {
|
||||
str: String
|
||||
int: Int
|
||||
bool: Boolean
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
subscribeHelloScalars(str: String, int: Int, bool: Boolean): HelloScalars
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Unreferenced type implementing referenced interface
|
||||
*/
|
||||
public function testUnreferencedTypeImplementingReferencedInterface()
|
||||
{
|
||||
$body = '
|
||||
type Concrete implements Iface {
|
||||
key: String
|
||||
}
|
||||
|
||||
interface Iface {
|
||||
key: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
iface: Iface
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Unreferenced type implementing referenced union
|
||||
*/
|
||||
public function testUnreferencedTypeImplementingReferencedUnion()
|
||||
{
|
||||
$body = '
|
||||
type Concrete {
|
||||
key: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
union: Union
|
||||
}
|
||||
|
||||
union Union = Concrete
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Supports @deprecated
|
||||
*/
|
||||
public function testSupportsDeprecated()
|
||||
{
|
||||
$body = '
|
||||
enum MyEnum {
|
||||
VALUE
|
||||
OLD_VALUE @deprecated
|
||||
OTHER_VALUE @deprecated(reason: "Terrible reasons")
|
||||
}
|
||||
|
||||
type Query {
|
||||
field1: String @deprecated
|
||||
field2: Int @deprecated(reason: "Because I said so")
|
||||
enum: MyEnum
|
||||
}
|
||||
';
|
||||
$output = $this->cycleOutput($body);
|
||||
$this->assertEquals($output, $body);
|
||||
|
||||
$ast = Parser::parse($body);
|
||||
$schema = BuildSchema::buildAST($ast);
|
||||
|
||||
$this->assertEquals($schema->getType('MyEnum')->getValues(), [
|
||||
new EnumValueDefinition([
|
||||
'name' => 'VALUE',
|
||||
'description' => '',
|
||||
'deprecationReason' => null,
|
||||
'value' => 'VALUE'
|
||||
]),
|
||||
new EnumValueDefinition([
|
||||
'name' => 'OLD_VALUE',
|
||||
'description' => '',
|
||||
'deprecationReason' => 'No longer supported',
|
||||
'value' => 'OLD_VALUE'
|
||||
]),
|
||||
new EnumValueDefinition([
|
||||
'name' => 'OTHER_VALUE',
|
||||
'description' => '',
|
||||
'deprecationReason' => 'Terrible reasons',
|
||||
'value' => 'OTHER_VALUE'
|
||||
])
|
||||
]);
|
||||
|
||||
$rootFields = $schema->getType('Query')->getFields();
|
||||
$this->assertEquals($rootFields['field1']->isDeprecated(), true);
|
||||
$this->assertEquals($rootFields['field1']->deprecationReason, 'No longer supported');
|
||||
|
||||
$this->assertEquals($rootFields['field2']->isDeprecated(), true);
|
||||
$this->assertEquals($rootFields['field2']->deprecationReason, 'Because I said so');
|
||||
}
|
||||
|
||||
// Describe: Failures
|
||||
|
||||
/**
|
||||
* @it Requires a schema definition or Query type
|
||||
*/
|
||||
public function testRequiresSchemaDefinitionOrQueryType()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Must provide schema definition with query type or a type named Query.');
|
||||
$body = '
|
||||
type Hello {
|
||||
bar: Bar
|
||||
}
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows only a single schema definition
|
||||
*/
|
||||
public function testAllowsOnlySingleSchemaDefinition()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Must provide only one schema definition.');
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
type Hello {
|
||||
bar: Bar
|
||||
}
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Requires a query type
|
||||
*/
|
||||
public function testRequiresQueryType()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Must provide schema definition with query type or a type named Query.');
|
||||
$body = '
|
||||
schema {
|
||||
mutation: Hello
|
||||
}
|
||||
|
||||
type Hello {
|
||||
bar: Bar
|
||||
}
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows only a single query type
|
||||
*/
|
||||
public function testAllowsOnlySingleQueryType()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Must provide only one query type in schema.');
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
query: Yellow
|
||||
}
|
||||
|
||||
type Hello {
|
||||
bar: Bar
|
||||
}
|
||||
|
||||
type Yellow {
|
||||
isColor: Boolean
|
||||
}
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows only a single mutation type
|
||||
*/
|
||||
public function testAllowsOnlySingleMutationType()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Must provide only one mutation type in schema.');
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
mutation: Hello
|
||||
mutation: Yellow
|
||||
}
|
||||
|
||||
type Hello {
|
||||
bar: Bar
|
||||
}
|
||||
|
||||
type Yellow {
|
||||
isColor: Boolean
|
||||
}
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows only a single subscription type
|
||||
*/
|
||||
public function testAllowsOnlySingleSubscriptionType()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Must provide only one subscription type in schema.');
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
subscription: Hello
|
||||
subscription: Yellow
|
||||
}
|
||||
|
||||
type Hello {
|
||||
bar: Bar
|
||||
}
|
||||
|
||||
type Yellow {
|
||||
isColor: Boolean
|
||||
}
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Unknown type referenced
|
||||
*/
|
||||
public function testUnknownTypeReferenced()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Type "Bar" not found in document.');
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
type Hello {
|
||||
bar: Bar
|
||||
}
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Unknown type in interface list
|
||||
*/
|
||||
public function testUnknownTypeInInterfaceList()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Type "Bar" not found in document.');
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
type Hello implements Bar { }
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Unknown type in union list
|
||||
*/
|
||||
public function testUnknownTypeInUnionList()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Type "Bar" not found in document.');
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
}
|
||||
|
||||
union TestUnion = Bar
|
||||
type Hello { testUnion: TestUnion }
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Unknown query type
|
||||
*/
|
||||
public function testUnknownQueryType()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Specified query type "Wat" not found in document.');
|
||||
$body = '
|
||||
schema {
|
||||
query: Wat
|
||||
}
|
||||
|
||||
type Hello {
|
||||
str: String
|
||||
}
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Unknown mutation type
|
||||
*/
|
||||
public function testUnknownMutationType()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Specified mutation type "Wat" not found in document.');
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
mutation: Wat
|
||||
}
|
||||
|
||||
type Hello {
|
||||
str: String
|
||||
}
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Unknown subscription type
|
||||
*/
|
||||
public function testUnknownSubscriptionType()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Specified subscription type "Awesome" not found in document.');
|
||||
$body = '
|
||||
schema {
|
||||
query: Hello
|
||||
mutation: Wat
|
||||
subscription: Awesome
|
||||
}
|
||||
|
||||
type Hello {
|
||||
str: String
|
||||
}
|
||||
|
||||
type Wat {
|
||||
str: String
|
||||
}
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Does not consider operation names
|
||||
*/
|
||||
public function testDoesNotConsiderOperationNames()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Specified query type "Foo" not found in document.');
|
||||
$body = '
|
||||
schema {
|
||||
query: Foo
|
||||
}
|
||||
|
||||
query Foo { field }
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Does not consider fragment names
|
||||
*/
|
||||
public function testDoesNotConsiderFragmentNames()
|
||||
{
|
||||
$this->setExpectedException('GraphQL\Error\Error', 'Specified query type "Foo" not found in document.');
|
||||
$body = '
|
||||
schema {
|
||||
query: Foo
|
||||
}
|
||||
|
||||
fragment Foo on Type { field }
|
||||
';
|
||||
$doc = Parser::parse($body);
|
||||
BuildSchema::buildAST($doc);
|
||||
}
|
||||
}
|
850
tests/Utils/SchemaPrinterTest.php
Normal file
850
tests/Utils/SchemaPrinterTest.php
Normal file
@ -0,0 +1,850 @@
|
||||
<?php
|
||||
namespace GraphQL\Tests\Utils;
|
||||
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\CustomScalarType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Utils\SchemaPrinter;
|
||||
|
||||
class SchemaPrinterTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// Describe: Type System Printer
|
||||
|
||||
private function printForTest($schema)
|
||||
{
|
||||
return "\n" . SchemaPrinter::doPrint($schema);
|
||||
}
|
||||
|
||||
private function printSingleFieldSchema($fieldConfig)
|
||||
{
|
||||
$root = new ObjectType([
|
||||
'name' => 'Root',
|
||||
'fields' => [
|
||||
'singleField' => $fieldConfig
|
||||
]
|
||||
]);
|
||||
return $this->printForTest(new Schema(['query' => $root]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints String Field
|
||||
*/
|
||||
public function testPrintsStringField()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::string()
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField: String
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints [String] Field
|
||||
*/
|
||||
public function testPrintArrayStringField()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::listOf(Type::string())
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField: [String]
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints String! Field
|
||||
*/
|
||||
public function testPrintNonNullStringField()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::nonNull(Type::string())
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField: String!
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints [String]! Field
|
||||
*/
|
||||
public function testPrintNonNullArrayStringField()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::nonNull(Type::listOf(Type::string()))
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField: [String]!
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints [String!] Field
|
||||
*/
|
||||
public function testPrintArrayNonNullStringField()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::listOf(Type::nonNull(Type::string()))
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField: [String!]
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints [String!]! Field
|
||||
*/
|
||||
public function testPrintNonNullArrayNonNullStringField()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::nonNull(Type::listOf(Type::nonNull(Type::string())))
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField: [String!]!
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints Object Field
|
||||
*/
|
||||
public function testPrintObjectField()
|
||||
{
|
||||
$fooType = new ObjectType([
|
||||
'name' => 'Foo',
|
||||
'fields' => ['str' => ['type' => Type::string()]]
|
||||
]);
|
||||
|
||||
$root = new ObjectType([
|
||||
'name' => 'Root',
|
||||
'fields' => ['foo' => ['type' => $fooType]]
|
||||
]);
|
||||
|
||||
$schema = new Schema(['query' => $root]);
|
||||
$output = $this->printForTest($schema);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Foo {
|
||||
str: String
|
||||
}
|
||||
|
||||
type Root {
|
||||
foo: Foo
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints String Field With Int Arg
|
||||
*/
|
||||
public function testPrintsStringFieldWithIntArg()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::string(),
|
||||
'args' => ['argOne' => ['type' => Type::int()]]
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField(argOne: Int): String
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints String Field With Int Arg With Default
|
||||
*/
|
||||
public function testPrintsStringFieldWithIntArgWithDefault()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::string(),
|
||||
'args' => ['argOne' => ['type' => Type::int(), 'defaultValue' => 2]]
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField(argOne: Int = 2): String
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints String Field With Int Arg With Default Null
|
||||
*/
|
||||
public function testPrintsStringFieldWithIntArgWithDefaultNull()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::string(),
|
||||
'args' => ['argOne' => ['type' => Type::int(), 'defaultValue' => null]]
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField(argOne: Int = null): String
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints String Field With Int! Arg
|
||||
*/
|
||||
public function testPrintsStringFieldWithNonNullIntArg()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::string(),
|
||||
'args' => ['argOne' => ['type' => Type::nonNull(Type::int())]]
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField(argOne: Int!): String
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints String Field With Multiple Args
|
||||
*/
|
||||
public function testPrintsStringFieldWithMultipleArgs()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::string(),
|
||||
'args' => [
|
||||
'argOne' => ['type' => Type::int()],
|
||||
'argTwo' => ['type' => Type::string()]
|
||||
]
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField(argOne: Int, argTwo: String): String
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints String Field With Multiple Args, First is Default
|
||||
*/
|
||||
public function testPrintsStringFieldWithMultipleArgsFirstIsDefault()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::string(),
|
||||
'args' => [
|
||||
'argOne' => ['type' => Type::int(), 'defaultValue' => 1],
|
||||
'argTwo' => ['type' => Type::string()],
|
||||
'argThree' => ['type' => Type::boolean()]
|
||||
]
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField(argOne: Int = 1, argTwo: String, argThree: Boolean): String
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints String Field With Multiple Args, Second is Default
|
||||
*/
|
||||
public function testPrintsStringFieldWithMultipleArgsSecondIsDefault()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::string(),
|
||||
'args' => [
|
||||
'argOne' => ['type' => Type::int()],
|
||||
'argTwo' => ['type' => Type::string(), 'defaultValue' => 'foo'],
|
||||
'argThree' => ['type' => Type::boolean()]
|
||||
]
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField(argOne: Int, argTwo: String = "foo", argThree: Boolean): String
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Prints String Field With Multiple Args, Last is Default
|
||||
*/
|
||||
public function testPrintsStringFieldWithMultipleArgsLastIsDefault()
|
||||
{
|
||||
$output = $this->printSingleFieldSchema([
|
||||
'type' => Type::string(),
|
||||
'args' => [
|
||||
'argOne' => ['type' => Type::int()],
|
||||
'argTwo' => ['type' => Type::string()],
|
||||
'argThree' => ['type' => Type::boolean(), 'defaultValue' => false]
|
||||
]
|
||||
]);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Root {
|
||||
singleField(argOne: Int, argTwo: String, argThree: Boolean = false): String
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Print Interface
|
||||
*/
|
||||
public function testPrintInterface()
|
||||
{
|
||||
$fooType = new InterfaceType([
|
||||
'name' => 'Foo',
|
||||
'resolveType' => function() { return null; },
|
||||
'fields' => ['str' => ['type' => Type::string()]]
|
||||
]);
|
||||
|
||||
$barType = new ObjectType([
|
||||
'name' => 'Bar',
|
||||
'fields' => ['str' => ['type' => Type::string()]],
|
||||
'interfaces' => [$fooType]
|
||||
]);
|
||||
|
||||
$root = new ObjectType([
|
||||
'name' => 'Root',
|
||||
'fields' => ['bar' => ['type' => $barType]]
|
||||
]);
|
||||
|
||||
$schema = new Schema([
|
||||
'query' => $root,
|
||||
'types' => [$barType]
|
||||
]);
|
||||
$output = $this->printForTest($schema);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Bar implements Foo {
|
||||
str: String
|
||||
}
|
||||
|
||||
interface Foo {
|
||||
str: String
|
||||
}
|
||||
|
||||
type Root {
|
||||
bar: Bar
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Print Multiple Interface
|
||||
*/
|
||||
public function testPrintMultipleInterface()
|
||||
{
|
||||
$fooType = new InterfaceType([
|
||||
'name' => 'Foo',
|
||||
'resolveType' => function() { return null; },
|
||||
'fields' => ['str' => ['type' => Type::string()]]
|
||||
]);
|
||||
|
||||
$baazType = new InterfaceType([
|
||||
'name' => 'Baaz',
|
||||
'resolveType' => function() { return null; },
|
||||
'fields' => ['int' => ['type' => Type::int()]]
|
||||
]);
|
||||
|
||||
$barType = new ObjectType([
|
||||
'name' => 'Bar',
|
||||
'fields' => [
|
||||
'str' => ['type' => Type::string()],
|
||||
'int' => ['type' => Type::int()]
|
||||
],
|
||||
'interfaces' => [$fooType, $baazType]
|
||||
]);
|
||||
|
||||
$root = new ObjectType([
|
||||
'name' => 'Root',
|
||||
'fields' => ['bar' => ['type' => $barType]]
|
||||
]);
|
||||
|
||||
$schema = new Schema([
|
||||
'query' => $root,
|
||||
'types' => [$barType]
|
||||
]);
|
||||
$output = $this->printForTest($schema);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
interface Baaz {
|
||||
int: Int
|
||||
}
|
||||
|
||||
type Bar implements Foo, Baaz {
|
||||
str: String
|
||||
int: Int
|
||||
}
|
||||
|
||||
interface Foo {
|
||||
str: String
|
||||
}
|
||||
|
||||
type Root {
|
||||
bar: Bar
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Print Unions
|
||||
*/
|
||||
public function testPrintUnions()
|
||||
{
|
||||
$fooType = new ObjectType([
|
||||
'name' => 'Foo',
|
||||
'fields' => ['bool' => ['type' => Type::boolean()]]
|
||||
]);
|
||||
|
||||
$barType = new ObjectType([
|
||||
'name' => 'Bar',
|
||||
'fields' => ['str' => ['type' => Type::string()]]
|
||||
]);
|
||||
|
||||
$singleUnion = new UnionType([
|
||||
'name' => 'SingleUnion',
|
||||
'resolveType' => function() { return null; },
|
||||
'types' => [$fooType]
|
||||
]);
|
||||
|
||||
$multipleUnion = new UnionType([
|
||||
'name' => 'MultipleUnion',
|
||||
'resolveType' => function() { return null; },
|
||||
'types' => [$fooType, $barType]
|
||||
]);
|
||||
|
||||
$root = new ObjectType([
|
||||
'name' => 'Root',
|
||||
'fields' => [
|
||||
'single' => ['type' => $singleUnion],
|
||||
'multiple' => ['type' => $multipleUnion]
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema(['query' => $root]);
|
||||
$output = $this->printForTest($schema);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
type Bar {
|
||||
str: String
|
||||
}
|
||||
|
||||
type Foo {
|
||||
bool: Boolean
|
||||
}
|
||||
|
||||
union MultipleUnion = Foo | Bar
|
||||
|
||||
type Root {
|
||||
single: SingleUnion
|
||||
multiple: MultipleUnion
|
||||
}
|
||||
|
||||
union SingleUnion = Foo
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Print Input Type
|
||||
*/
|
||||
public function testInputType()
|
||||
{
|
||||
$inputType = new InputObjectType([
|
||||
'name' => 'InputType',
|
||||
'fields' => ['int' => ['type' => Type::int()]]
|
||||
]);
|
||||
|
||||
$root = new ObjectType([
|
||||
'name' => 'Root',
|
||||
'fields' => [
|
||||
'str' => [
|
||||
'type' => Type::string(),
|
||||
'args' => ['argOne' => ['type' => $inputType]]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema(['query' => $root]);
|
||||
$output = $this->printForTest($schema);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
input InputType {
|
||||
int: Int
|
||||
}
|
||||
|
||||
type Root {
|
||||
str(argOne: InputType): String
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Custom Scalar
|
||||
*/
|
||||
public function testCustomScalar()
|
||||
{
|
||||
$oddType = new CustomScalarType([
|
||||
'name' => 'Odd',
|
||||
'serialize' => function($value) {
|
||||
return $value % 2 === 1 ? $value : null;
|
||||
}
|
||||
]);
|
||||
|
||||
$root = new ObjectType([
|
||||
'name' => 'Root',
|
||||
'fields' => [
|
||||
'odd' => ['type' => $oddType]
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema(['query' => $root]);
|
||||
$output = $this->printForTest($schema);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
scalar Odd
|
||||
|
||||
type Root {
|
||||
odd: Odd
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Enum
|
||||
*/
|
||||
public function testEnum()
|
||||
{
|
||||
$RGBType = new EnumType([
|
||||
'name' => 'RGB',
|
||||
'values' => [
|
||||
'RED' => ['value' => 0],
|
||||
'GREEN' => ['value' => 1],
|
||||
'BLUE' => ['value' => 2]
|
||||
]
|
||||
]);
|
||||
|
||||
$root = new ObjectType([
|
||||
'name' => 'Root',
|
||||
'fields' => [
|
||||
'rgb' => ['type' => $RGBType]
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema(['query' => $root]);
|
||||
$output = $this->printForTest($schema);
|
||||
$this->assertEquals($output, '
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
enum RGB {
|
||||
RED
|
||||
GREEN
|
||||
BLUE
|
||||
}
|
||||
|
||||
type Root {
|
||||
rgb: RGB
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Print Introspection Schema
|
||||
*/
|
||||
public function testPrintIntrospectionSchema()
|
||||
{
|
||||
$root = new ObjectType([
|
||||
'name' => 'Root',
|
||||
'fields' => [
|
||||
'onlyField' => ['type' => Type::string()]
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema(['query' => $root]);
|
||||
$output = SchemaPrinter::printIntrosepctionSchema($schema);
|
||||
$introspectionSchema = <<<'EOT'
|
||||
schema {
|
||||
query: Root
|
||||
}
|
||||
|
||||
# Directs the executor to include this field or fragment only when the `if` argument is true.
|
||||
directive @include(
|
||||
# Included when true.
|
||||
if: Boolean!
|
||||
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
|
||||
|
||||
# Directs the executor to skip this field or fragment when the `if` argument is true.
|
||||
directive @skip(
|
||||
# Skipped when true.
|
||||
if: Boolean!
|
||||
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
|
||||
|
||||
# Marks an element of a GraphQL schema as no longer supported.
|
||||
directive @deprecated(
|
||||
# Explains why this element was deprecated, usually also including a suggestion
|
||||
# for how to access supported similar data. Formatted in
|
||||
# [Markdown](https://daringfireball.net/projects/markdown/).
|
||||
reason: String = "No longer supported"
|
||||
) on FIELD_DEFINITION | ENUM_VALUE
|
||||
|
||||
# A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.
|
||||
#
|
||||
# In some cases, you need to provide options to alter GraphQL's execution behavior
|
||||
# in ways field arguments will not suffice, such as conditionally including or
|
||||
# skipping a field. Directives provide this by describing additional information
|
||||
# to the executor.
|
||||
type __Directive {
|
||||
name: String!
|
||||
description: String
|
||||
locations: [__DirectiveLocation!]!
|
||||
args: [__InputValue!]!
|
||||
onOperation: Boolean! @deprecated(reason: "Use `locations`.")
|
||||
onFragment: Boolean! @deprecated(reason: "Use `locations`.")
|
||||
onField: Boolean! @deprecated(reason: "Use `locations`.")
|
||||
}
|
||||
|
||||
# A Directive can be adjacent to many parts of the GraphQL language, a
|
||||
# __DirectiveLocation describes one such possible adjacencies.
|
||||
enum __DirectiveLocation {
|
||||
# Location adjacent to a query operation.
|
||||
QUERY
|
||||
|
||||
# Location adjacent to a mutation operation.
|
||||
MUTATION
|
||||
|
||||
# Location adjacent to a subscription operation.
|
||||
SUBSCRIPTION
|
||||
|
||||
# Location adjacent to a field.
|
||||
FIELD
|
||||
|
||||
# Location adjacent to a fragment definition.
|
||||
FRAGMENT_DEFINITION
|
||||
|
||||
# Location adjacent to a fragment spread.
|
||||
FRAGMENT_SPREAD
|
||||
|
||||
# Location adjacent to an inline fragment.
|
||||
INLINE_FRAGMENT
|
||||
|
||||
# Location adjacent to a schema definition.
|
||||
SCHEMA
|
||||
|
||||
# Location adjacent to a scalar definition.
|
||||
SCALAR
|
||||
|
||||
# Location adjacent to an object type definition.
|
||||
OBJECT
|
||||
|
||||
# Location adjacent to a field definition.
|
||||
FIELD_DEFINITION
|
||||
|
||||
# Location adjacent to an argument definition.
|
||||
ARGUMENT_DEFINITION
|
||||
|
||||
# Location adjacent to an interface definition.
|
||||
INTERFACE
|
||||
|
||||
# Location adjacent to a union definition.
|
||||
UNION
|
||||
|
||||
# Location adjacent to an enum definition.
|
||||
ENUM
|
||||
|
||||
# Location adjacent to an enum value definition.
|
||||
ENUM_VALUE
|
||||
|
||||
# Location adjacent to an input object type definition.
|
||||
INPUT_OBJECT
|
||||
|
||||
# Location adjacent to an input object field definition.
|
||||
INPUT_FIELD_DEFINITION
|
||||
}
|
||||
|
||||
# One possible value for a given Enum. Enum values are unique values, not a
|
||||
# placeholder for a string or numeric value. However an Enum value is returned in
|
||||
# a JSON response as a string.
|
||||
type __EnumValue {
|
||||
name: String!
|
||||
description: String
|
||||
isDeprecated: Boolean!
|
||||
deprecationReason: String
|
||||
}
|
||||
|
||||
# Object and Interface types are described by a list of Fields, each of which has
|
||||
# a name, potentially a list of arguments, and a return type.
|
||||
type __Field {
|
||||
name: String!
|
||||
description: String
|
||||
args: [__InputValue!]!
|
||||
type: __Type!
|
||||
isDeprecated: Boolean!
|
||||
deprecationReason: String
|
||||
}
|
||||
|
||||
# Arguments provided to Fields or Directives and the input fields of an
|
||||
# InputObject are represented as Input Values which describe their type and
|
||||
# optionally a default value.
|
||||
type __InputValue {
|
||||
name: String!
|
||||
description: String
|
||||
type: __Type!
|
||||
|
||||
# A GraphQL-formatted string representing the default value for this input value.
|
||||
defaultValue: String
|
||||
}
|
||||
|
||||
# A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all
|
||||
# available types and directives on the server, as well as the entry points for
|
||||
# query, mutation, and subscription operations.
|
||||
type __Schema {
|
||||
# A list of all types supported by this server.
|
||||
types: [__Type!]!
|
||||
|
||||
# The type that query operations will be rooted at.
|
||||
queryType: __Type!
|
||||
|
||||
# If this server supports mutation, the type that mutation operations will be rooted at.
|
||||
mutationType: __Type
|
||||
|
||||
# If this server support subscription, the type that subscription operations will be rooted at.
|
||||
subscriptionType: __Type
|
||||
|
||||
# A list of all directives supported by this server.
|
||||
directives: [__Directive!]!
|
||||
}
|
||||
|
||||
# The fundamental unit of any GraphQL Schema is the type. There are many kinds of
|
||||
# types in GraphQL as represented by the `__TypeKind` enum.
|
||||
#
|
||||
# Depending on the kind of a type, certain fields describe information about that
|
||||
# type. Scalar types provide no information beyond a name and description, while
|
||||
# Enum types provide their values. Object and Interface types provide the fields
|
||||
# they describe. Abstract types, Union and Interface, provide the Object types
|
||||
# possible at runtime. List and NonNull types compose other types.
|
||||
type __Type {
|
||||
kind: __TypeKind!
|
||||
name: String
|
||||
description: String
|
||||
fields(includeDeprecated: Boolean = false): [__Field!]
|
||||
interfaces: [__Type!]
|
||||
possibleTypes: [__Type!]
|
||||
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
|
||||
inputFields: [__InputValue!]
|
||||
ofType: __Type
|
||||
}
|
||||
|
||||
# An enum describing what kind of type a given `__Type` is.
|
||||
enum __TypeKind {
|
||||
# Indicates this type is a scalar.
|
||||
SCALAR
|
||||
|
||||
# Indicates this type is an object. `fields` and `interfaces` are valid fields.
|
||||
OBJECT
|
||||
|
||||
# Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.
|
||||
INTERFACE
|
||||
|
||||
# Indicates this type is a union. `possibleTypes` is a valid field.
|
||||
UNION
|
||||
|
||||
# Indicates this type is an enum. `enumValues` is a valid field.
|
||||
ENUM
|
||||
|
||||
# Indicates this type is an input object. `inputFields` is a valid field.
|
||||
INPUT_OBJECT
|
||||
|
||||
# Indicates this type is a list. `ofType` is a valid field.
|
||||
LIST
|
||||
|
||||
# Indicates this type is a non-null. `ofType` is a valid field.
|
||||
NON_NULL
|
||||
}
|
||||
|
||||
EOT;
|
||||
$this->assertEquals($output, $introspectionSchema);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user