Merge pull request #91 from petecoop/build-schema

Build Schema & Schema Printer
This commit is contained in:
Vladimir Razuvaev 2017-02-21 15:25:14 +07:00 committed by GitHub
commit ed41a4ce43
9 changed files with 2599 additions and 8 deletions

View File

@ -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;

View File

@ -107,7 +107,7 @@ class Directive
new FieldArgument([
'name' => 'if',
'type' => Type::nonNull(Type::boolean()),
'description' => 'Skipped when true'
'description' => 'Skipped when true.'
])
]
]),

View File

@ -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 GraphQLs ' .
"\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,

View File

@ -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
View 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
View 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);
}
}

View File

@ -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.',

View 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);
}
}

View 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);
}
}