Move schema validation into separate step (type constructors)

This is the second step of moving work from type constructors to the schema validation function.

ref: graphql/graphql-js#1132
This commit is contained in:
Daniel Tschinder 2018-02-15 12:14:08 +01:00
parent 6d08c342c9
commit 97e8a9e200
23 changed files with 1402 additions and 2053 deletions

View File

@ -171,7 +171,7 @@ static function float()
```php ```php
/** /**
* @api * @api
* @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType * @param Type|ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
* @return ListOfType * @return ListOfType
*/ */
static function listOf($wrappedType) static function listOf($wrappedType)
@ -1345,7 +1345,6 @@ Also it is possible to override warning handler (which is **trigger_error()** by
**Class Constants:** **Class Constants:**
```php ```php
const WARNING_NAME = 1;
const WARNING_ASSIGN = 2; const WARNING_ASSIGN = 2;
const WARNING_CONFIG = 4; const WARNING_CONFIG = 4;
const WARNING_FULL_SCHEMA_SCAN = 8; const WARNING_FULL_SCHEMA_SCAN = 8;

View File

@ -9,7 +9,6 @@ namespace GraphQL\Error;
*/ */
final class Warning final class Warning
{ {
const WARNING_NAME = 1;
const WARNING_ASSIGN = 2; const WARNING_ASSIGN = 2;
const WARNING_CONFIG = 4; const WARNING_CONFIG = 4;
const WARNING_FULL_SCHEMA_SCAN = 8; const WARNING_FULL_SCHEMA_SCAN = 8;

View File

@ -19,7 +19,7 @@ class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode
public $directives; public $directives;
/** /**
* @var EnumValueDefinitionNode[]|null * @var EnumValueDefinitionNode[]|null|NodeList
*/ */
public $values; public $values;

View File

@ -14,7 +14,7 @@ class FieldDefinitionNode extends Node
public $name; public $name;
/** /**
* @var InputValueDefinitionNode[] * @var InputValueDefinitionNode[]|NodeList
*/ */
public $arguments; public $arguments;
@ -24,7 +24,7 @@ class FieldDefinitionNode extends Node
public $type; public $type;
/** /**
* @var DirectiveNode[] * @var DirectiveNode[]|NodeList
*/ */
public $directives; public $directives;

View File

@ -980,6 +980,7 @@ class Parser
/** /**
* @return OperationTypeDefinitionNode * @return OperationTypeDefinitionNode
* @throws SyntaxError
*/ */
function parseOperationTypeDefinition() function parseOperationTypeDefinition()
{ {
@ -1095,11 +1096,12 @@ class Parser
/** /**
* @return InputValueDefinitionNode[]|NodeList * @return InputValueDefinitionNode[]|NodeList
* @throws SyntaxError
*/ */
function parseArgumentDefs() function parseArgumentDefs()
{ {
if (!$this->peek(Token::PAREN_L)) { if (!$this->peek(Token::PAREN_L)) {
return []; return new NodeList([]);
} }
return $this->many(Token::PAREN_L, [$this, 'parseInputValueDef'], Token::PAREN_R); return $this->many(Token::PAREN_L, [$this, 'parseInputValueDef'], Token::PAREN_R);
} }
@ -1357,7 +1359,7 @@ class Parser
$fields = $this->parseFieldsDefinition(); $fields = $this->parseFieldsDefinition();
if ( if (
count($interfaces) === 0 && !$interfaces &&
count($directives) === 0 && count($directives) === 0 &&
count($fields) === 0 count($fields) === 0
) { ) {
@ -1412,7 +1414,7 @@ class Parser
$types = $this->parseMemberTypesDefinition(); $types = $this->parseMemberTypesDefinition();
if ( if (
count($directives) === 0 && count($directives) === 0 &&
count($types) === 0 !$types
) { ) {
throw $this->unexpected(); throw $this->unexpected();
} }

View File

@ -3,6 +3,7 @@ namespace GraphQL\Type\Definition;
use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\DirectiveLocation; use GraphQL\Language\DirectiveLocation;
use GraphQL\Utils\Utils;
/** /**
* Class Directive * Class Directive
@ -159,6 +160,9 @@ class Directive
foreach ($config as $key => $value) { foreach ($config as $key => $value) {
$this->{$key} = $value; $this->{$key} = $value;
} }
Utils::invariant($this->name, 'Directive must be named.');
Utils::invariant(is_array($this->locations), 'Must provide locations for directive.');
$this->config = $config; $this->config = $config;
} }
} }

View File

@ -11,7 +11,7 @@ use GraphQL\Utils\Utils;
* Class EnumType * Class EnumType
* @package GraphQL\Type\Definition * @package GraphQL\Type\Definition
*/ */
class EnumType extends Type implements InputType, OutputType, LeafType class EnumType extends Type implements InputType, OutputType, LeafType, NamedType
{ {
/** /**
* @var EnumTypeDefinitionNode|null * @var EnumTypeDefinitionNode|null
@ -39,7 +39,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name'], !empty($config['isIntrospection'])); Utils::invariant(is_string($config['name']), 'Must provide name.');
Config::validate($config, [ Config::validate($config, [
'name' => Config::NAME | Config::REQUIRED, 'name' => Config::NAME | Config::REQUIRED,
@ -188,24 +188,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType
); );
$values = $this->getValues(); $values = $this->getValues();
Utils::invariant(
!empty($values),
"{$this->name} values must be not empty."
);
foreach ($values as $value) { foreach ($values as $value) {
try {
Utils::assertValidName($value->name);
} catch (InvariantViolation $e) {
throw new InvariantViolation(
"{$this->name} has value with invalid name: " .
Utils::printSafe($value->name) . " ({$e->getMessage()})"
);
}
Utils::invariant(
!in_array($value->name, ['true', 'false', 'null']),
"{$this->name}: \"{$value->name}\" can not be used as an Enum value."
);
Utils::invariant( Utils::invariant(
!isset($value->config['isDeprecated']), !isset($value->config['isDeprecated']),
"{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"." "{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"."

View File

@ -9,7 +9,7 @@ use GraphQL\Utils\Utils;
* Class InputObjectType * Class InputObjectType
* @package GraphQL\Type\Definition * @package GraphQL\Type\Definition
*/ */
class InputObjectType extends Type implements InputType class InputObjectType extends Type implements InputType, NamedType
{ {
/** /**
* @var InputObjectField[] * @var InputObjectField[]
@ -31,7 +31,7 @@ class InputObjectType extends Type implements InputType
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name']); Utils::invariant(is_string($config['name']), 'Must provide name.');
Config::validate($config, [ Config::validate($config, [
'name' => Config::NAME | Config::REQUIRED, 'name' => Config::NAME | Config::REQUIRED,
@ -91,41 +91,4 @@ class InputObjectType extends Type implements InputType
Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name); Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name);
return $this->fields[$name]; return $this->fields[$name];
} }
/**
* @throws InvariantViolation
*/
public function assertValid()
{
parent::assertValid();
$fields = $this->getFields();
Utils::invariant(
!empty($fields),
"{$this->name} fields must not be empty"
);
foreach ($fields as $field) {
try {
Utils::assertValidName($field->name);
} catch (InvariantViolation $e) {
throw new InvariantViolation("{$this->name}.{$field->name}: {$e->getMessage()}");
}
$fieldType = $field->type;
if ($fieldType instanceof WrappingType) {
$fieldType = $fieldType->getWrappedType(true);
}
Utils::invariant(
$fieldType instanceof InputType,
"{$this->name}.{$field->name} field type must be Input Type but got: %s.",
Utils::printSafe($field->type)
);
Utils::invariant(
!isset($field->config['resolve']),
"{$this->name}.{$field->name} field type has a resolve property, but Input Types cannot define resolvers."
);
}
}
} }

View File

@ -3,11 +3,16 @@ namespace GraphQL\Type\Definition;
/* /*
export type GraphQLInputType = export type GraphQLInputType =
GraphQLScalarType | | GraphQLScalarType
GraphQLEnumType | | GraphQLEnumType
GraphQLInputObjectType | | GraphQLInputObjectType
GraphQLList | | GraphQLList<GraphQLInputType>
GraphQLNonNull; | GraphQLNonNull<
| GraphQLScalarType
| GraphQLEnumType
| GraphQLInputObjectType
| GraphQLList<GraphQLInputType>,
>;
*/ */
interface InputType interface InputType
{ {

View File

@ -10,7 +10,7 @@ use GraphQL\Utils\Utils;
* Class InterfaceType * Class InterfaceType
* @package GraphQL\Type\Definition * @package GraphQL\Type\Definition
*/ */
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NamedType
{ {
/** /**
* @param mixed $type * @param mixed $type
@ -51,7 +51,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name']); Utils::invariant(is_string($config['name']), 'Must provide name.');
Config::validate($config, [ Config::validate($config, [
'name' => Config::NAME, 'name' => Config::NAME,
@ -120,23 +120,9 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
{ {
parent::assertValid(); parent::assertValid();
$fields = $this->getFields();
Utils::invariant( Utils::invariant(
!isset($this->config['resolveType']) || is_callable($this->config['resolveType']), !isset($this->config['resolveType']) || is_callable($this->config['resolveType']),
"{$this->name} must provide \"resolveType\" as a function." "{$this->name} must provide \"resolveType\" as a function."
); );
Utils::invariant(
!empty($fields),
"{$this->name} fields must not be empty"
);
foreach ($fields as $field) {
$field->assertValid($this);
foreach ($field->args as $arg) {
$arg->assertValid($field, $this);
}
}
} }
} }

View File

@ -0,0 +1,15 @@
<?php
namespace GraphQL\Type\Definition;
/*
export type GraphQLNamedType =
| GraphQLScalarType
| GraphQLObjectType
| GraphQLInterfaceType
| GraphQLUnionType
| GraphQLEnumType
| GraphQLInputObjectType;
*/
interface NamedType
{
}

View File

@ -47,7 +47,7 @@ use GraphQL\Utils\Utils;
* ]); * ]);
* *
*/ */
class ObjectType extends Type implements OutputType, CompositeType class ObjectType extends Type implements OutputType, CompositeType, NamedType
{ {
/** /**
* @param mixed $type * @param mixed $type
@ -103,7 +103,7 @@ class ObjectType extends Type implements OutputType, CompositeType
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name'], !empty($config['isIntrospection'])); Utils::invariant(is_string($config['name']), 'Must provide name.');
// Note: this validation is disabled by default, because it is resource-consuming // Note: this validation is disabled by default, because it is resource-consuming
// TODO: add bin/validate script to check if schema is valid during development // TODO: add bin/validate script to check if schema is valid during development
@ -228,18 +228,5 @@ class ObjectType extends Type implements OutputType, CompositeType
!isset($this->config['isTypeOf']) || is_callable($this->config['isTypeOf']), !isset($this->config['isTypeOf']) || is_callable($this->config['isTypeOf']),
"{$this->name} must provide 'isTypeOf' as a function" "{$this->name} must provide 'isTypeOf' as a function"
); );
// getFields() and getInterfaceMap() will do structural validation
$fields = $this->getFields();
Utils::invariant(
!empty($fields),
"{$this->name} fields must not be empty"
);
foreach ($fields as $field) {
$field->assertValid($this);
foreach ($field->args as $arg) {
$arg->assertValid($field, $this);
}
}
} }
} }

View File

@ -22,7 +22,7 @@ use GraphQL\Utils\Utils;
* } * }
* } * }
*/ */
abstract class ScalarType extends Type implements OutputType, InputType, LeafType abstract class ScalarType extends Type implements OutputType, InputType, LeafType, NamedType
{ {
/** /**
* @var ScalarTypeDefinitionNode|null * @var ScalarTypeDefinitionNode|null
@ -36,6 +36,6 @@ abstract class ScalarType extends Type implements OutputType, InputType, LeafTyp
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null; $this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
$this->config = $config; $this->config = $config;
Utils::assertValidName($this->name); Utils::invariant(is_string($this->name), 'Must provide name.');
} }
} }

View File

@ -80,7 +80,7 @@ abstract class Type implements \JsonSerializable
/** /**
* @api * @api
* @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType * @param Type|ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
* @return ListOfType * @return ListOfType
*/ */
public static function listOf($wrappedType) public static function listOf($wrappedType)
@ -161,8 +161,11 @@ abstract class Type implements \JsonSerializable
*/ */
public static function isInputType($type) public static function isInputType($type)
{ {
$nakedType = self::getNamedType($type); return $type instanceof InputType &&
return $nakedType instanceof InputType; (
!$type instanceof WrappingType ||
self::getNamedType($type) instanceof InputType
);
} }
/** /**
@ -172,8 +175,11 @@ abstract class Type implements \JsonSerializable
*/ */
public static function isOutputType($type) public static function isOutputType($type)
{ {
$nakedType = self::getNamedType($type); return $type instanceof OutputType &&
return $nakedType instanceof OutputType; (
!$type instanceof WrappingType ||
self::getNamedType($type) instanceof OutputType
);
} }
/** /**
@ -311,6 +317,7 @@ abstract class Type implements \JsonSerializable
*/ */
public function assertValid() public function assertValid()
{ {
Utils::assertValidName($this->name);
} }
/** /**

View File

@ -9,7 +9,7 @@ use GraphQL\Utils\Utils;
* Class UnionType * Class UnionType
* @package GraphQL\Type\Definition * @package GraphQL\Type\Definition
*/ */
class UnionType extends Type implements AbstractType, OutputType, CompositeType class UnionType extends Type implements AbstractType, OutputType, CompositeType, NamedType
{ {
/** /**
* @var UnionTypeDefinitionNode * @var UnionTypeDefinitionNode
@ -36,7 +36,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name']); Utils::invariant(is_string($config['name']), 'Must provide name.');
Config::validate($config, [ Config::validate($config, [
'name' => Config::NAME | Config::REQUIRED, 'name' => Config::NAME | Config::REQUIRED,
@ -81,7 +81,8 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
if (!is_array($types)) { if (!is_array($types)) {
throw new InvariantViolation( throw new InvariantViolation(
"{$this->name} types must be an Array or a callable which returns an Array." "Must provide Array of types or a callable which returns " .
"such an array for Union {$this->name}"
); );
} }
@ -133,31 +134,11 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
{ {
parent::assertValid(); parent::assertValid();
$types = $this->getTypes();
Utils::invariant(
!empty($types),
"{$this->name} types must not be empty"
);
if (isset($this->config['resolveType'])) { if (isset($this->config['resolveType'])) {
Utils::invariant( Utils::invariant(
is_callable($this->config['resolveType']), is_callable($this->config['resolveType']),
"{$this->name} must provide \"resolveType\" as a function." "{$this->name} must provide \"resolveType\" as a function."
); );
} }
$includedTypeNames = [];
foreach ($types as $objType) {
Utils::invariant(
$objType instanceof ObjectType,
"{$this->name} may only contain Object types, it cannot contain: %s.",
Utils::printSafe($objType)
);
Utils::invariant(
!isset($includedTypeNames[$objType->name]),
"{$this->name} can include {$objType->name} type only once."
);
$includedTypeNames[$objType->name] = true;
}
} }
} }

View File

@ -168,7 +168,7 @@ EOD;
* @param Type $type * @param Type $type
* @return bool * @return bool
*/ */
public static function isIntrospectionType(Type $type) public static function isIntrospectionType($type)
{ {
return in_array($type->name, array_keys(self::getTypes())); return in_array($type->name, array_keys(self::getTypes()));
} }

View File

@ -2,9 +2,11 @@
namespace GraphQL\Type; namespace GraphQL\Type;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode; use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputValueDefinitionNode; use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
use GraphQL\Language\AST\NamedTypeNode; use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\AST\ObjectTypeDefinitionNode;
@ -13,20 +15,24 @@ use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\AST\TypeDefinitionNode; use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\AST\TypeNode; use GraphQL\Language\AST\TypeNode;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\EnumValueDefinition;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectField;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Utils\TypeComparators; use GraphQL\Utils\TypeComparators;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
/**
*
*/
class SchemaValidationContext class SchemaValidationContext
{ {
/** /**
* @var array * @var Error[]
*/ */
private $errors = []; private $errors = [];
@ -56,7 +62,7 @@ class SchemaValidationContext
); );
} else if (!$queryType instanceof ObjectType) { } else if (!$queryType instanceof ObjectType) {
$this->reportError( $this->reportError(
'Query root type must be Object type but got: ' . Utils::getVariableType($queryType) . '.', 'Query root type must be Object type, it cannot be ' . Utils::printSafe($queryType) . '.',
$this->getOperationTypeNode($queryType, 'query') $this->getOperationTypeNode($queryType, 'query')
); );
} }
@ -64,7 +70,7 @@ class SchemaValidationContext
$mutationType = $this->schema->getMutationType(); $mutationType = $this->schema->getMutationType();
if ($mutationType && !$mutationType instanceof ObjectType) { if ($mutationType && !$mutationType instanceof ObjectType) {
$this->reportError( $this->reportError(
'Mutation root type must be Object type if provided but got: ' . Utils::getVariableType($mutationType) . '.', 'Mutation root type must be Object type if provided, it cannot be ' . Utils::printSafe($mutationType) . '.',
$this->getOperationTypeNode($mutationType, 'mutation') $this->getOperationTypeNode($mutationType, 'mutation')
); );
} }
@ -72,282 +78,12 @@ class SchemaValidationContext
$subscriptionType = $this->schema->getSubscriptionType(); $subscriptionType = $this->schema->getSubscriptionType();
if ($subscriptionType && !$subscriptionType instanceof ObjectType) { if ($subscriptionType && !$subscriptionType instanceof ObjectType) {
$this->reportError( $this->reportError(
'Subscription root type must be Object type if provided but got: ' . Utils::getVariableType($subscriptionType) . '.', 'Subscription root type must be Object type if provided, it cannot be ' . Utils::printSafe($subscriptionType) . '.',
$this->getOperationTypeNode($subscriptionType, 'subscription') $this->getOperationTypeNode($subscriptionType, 'subscription')
); );
} }
} }
public function validateDirectives()
{
$directives = $this->schema->getDirectives();
foreach($directives as $directive) {
if (!$directive instanceof Directive) {
$this->reportError(
"Expected directive but got: " . $directive,
is_object($directive) ? $directive->astNode : null
);
}
}
}
public function validateTypes()
{
$typeMap = $this->schema->getTypeMap();
foreach($typeMap as $typeName => $type) {
// Ensure all provided types are in fact GraphQL type.
if (!Type::isType($type)) {
$this->reportError(
"Expected GraphQL type but got: " . Utils::getVariableType($type),
is_object($type) ? $type->astNode : null
);
}
// Ensure objects implement the interfaces they claim to.
if ($type instanceof ObjectType) {
$implementedTypeNames = [];
foreach($type->getInterfaces() as $iface) {
if (isset($implementedTypeNames[$iface->name])) {
$this->reportError(
"{$type->name} must declare it implements {$iface->name} only once.",
$this->getAllImplementsInterfaceNode($type, $iface)
);
}
$implementedTypeNames[$iface->name] = true;
$this->validateObjectImplementsInterface($type, $iface);
}
}
}
}
/**
* @param ObjectType $object
* @param InterfaceType $iface
*/
private function validateObjectImplementsInterface(ObjectType $object, $iface)
{
if (!$iface instanceof InterfaceType) {
$this->reportError(
$object .
" must only implement Interface types, it cannot implement " .
$iface . ".",
$this->getImplementsInterfaceNode($object, $iface)
);
return;
}
$objectFieldMap = $object->getFields();
$ifaceFieldMap = $iface->getFields();
// Assert each interface field is implemented.
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
$objectField = array_key_exists($fieldName, $objectFieldMap)
? $objectFieldMap[$fieldName]
: null;
// Assert interface field exists on object.
if (!$objectField) {
$this->reportError(
"\"{$iface->name}\" expects field \"{$fieldName}\" but \"{$object->name}\" does not provide it.",
[$this->getFieldNode($iface, $fieldName), $object->astNode]
);
continue;
}
// Assert interface field type is satisfied by object field type, by being
// a valid subtype. (covariant)
if (
!TypeComparators::isTypeSubTypeOf(
$this->schema,
$objectField->getType(),
$ifaceField->getType()
)
) {
$this->reportError(
"{$iface->name}.{$fieldName} expects type ".
"\"{$ifaceField->getType()}\"" .
" but {$object->name}.{$fieldName} is type " .
"\"{$objectField->getType()}\".",
[
$this->getFieldTypeNode($iface, $fieldName),
$this->getFieldTypeNode($object, $fieldName),
]
);
}
// Assert each interface field arg is implemented.
foreach($ifaceField->args as $ifaceArg) {
$argName = $ifaceArg->name;
$objectArg = null;
foreach($objectField->args as $arg) {
if ($arg->name === $argName) {
$objectArg = $arg;
break;
}
}
// Assert interface field arg exists on object field.
if (!$objectArg) {
$this->reportError(
"{$iface->name}.{$fieldName} expects argument \"{$argName}\" but ".
"{$object->name}.{$fieldName} does not provide it.",
[
$this->getFieldArgNode($iface, $fieldName, $argName),
$this->getFieldNode($object, $fieldName),
]
);
continue;
}
// Assert interface field arg type matches object field arg type.
// (invariant)
// TODO: change to contravariant?
if (!TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType())) {
$this->reportError(
"{$iface->name}.{$fieldName}({$argName}:) expects type ".
"\"{$ifaceArg->getType()}\"" .
" but {$object->name}.{$fieldName}({$argName}:) is type " .
"\"{$objectArg->getType()}\".",
[
$this->getFieldArgTypeNode($iface, $fieldName, $argName),
$this->getFieldArgTypeNode($object, $fieldName, $argName),
]
);
}
// TODO: validate default values?
}
// Assert additional arguments must not be required.
foreach($objectField->args as $objectArg) {
$argName = $objectArg->name;
$ifaceArg = null;
foreach($ifaceField->args as $arg) {
if ($arg->name === $argName) {
$ifaceArg = $arg;
break;
}
}
if (!$ifaceArg && $objectArg->getType() instanceof NonNull) {
$this->reportError(
"{$object->name}.{$fieldName}({$argName}:) is of required type " .
"\"{$objectArg->getType()}\"" .
" but is not also provided by the interface {$iface->name}.{$fieldName}.",
[
$this->getFieldArgTypeNode($object, $fieldName, $argName),
$this->getFieldNode($iface, $fieldName),
]
);
}
}
}
}
/**
* @param ObjectType $type
* @param InterfaceType|null $iface
* @return NamedTypeNode|null
*/
private function getImplementsInterfaceNode(ObjectType $type, $iface)
{
$nodes = $this->getAllImplementsInterfaceNode($type, $iface);
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
}
/**
* @param ObjectType $type
* @param InterfaceType|null $iface
* @return NamedTypeNode[]
*/
private function getAllImplementsInterfaceNode(ObjectType $type, $iface)
{
$implementsNodes = [];
/** @var ObjectTypeDefinitionNode|ObjectTypeExtensionNode[] $astNodes */
$astNodes = array_merge([$type->astNode], $type->extensionASTNodes ?: []);
foreach($astNodes as $astNode) {
if ($astNode && $astNode->interfaces) {
foreach($astNode->interfaces as $node) {
if ($node->name->value === $iface->name) {
$implementsNodes[] = $node;
}
}
}
}
return $implementsNodes;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @return FieldDefinitionNode|null
*/
private function getFieldNode($type, $fieldName)
{
/** @var ObjectTypeDefinitionNode|ObjectTypeExtensionNode[] $astNodes */
$astNodes = array_merge([$type->astNode], $type->extensionASTNodes ?: []);
foreach($astNodes as $astNode) {
if ($astNode && $astNode->fields) {
foreach($astNode->fields as $node) {
if ($node->name->value === $fieldName) {
return $node;
}
}
}
}
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @return TypeNode|null
*/
private function getFieldTypeNode($type, $fieldName)
{
$fieldNode = $this->getFieldNode($type, $fieldName);
if ($fieldNode) {
return $fieldNode->type;
}
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @param string $argName
* @return InputValueDefinitionNode|null
*/
private function getFieldArgNode($type, $fieldName, $argName)
{
$fieldNode = $this->getFieldNode($type, $fieldName);
if ($fieldNode && $fieldNode->arguments) {
foreach ($fieldNode->arguments as $node) {
if ($node->name->value === $argName) {
return $node;
}
}
}
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @param string $argName
* @return TypeNode|null
*/
private function getFieldArgTypeNode($type, $fieldName, $argName)
{
$fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName);
if ($fieldArgNode) {
return $fieldArgNode->type;
}
}
/** /**
* @param Type $type * @param Type $type
* @param string $operation * @param string $operation
@ -373,12 +109,620 @@ class SchemaValidationContext
return $operationTypeNode ? $operationTypeNode->type : ($type ? $type->astNode : null); return $operationTypeNode ? $operationTypeNode->type : ($type ? $type->astNode : null);
} }
public function validateDirectives()
{
$directives = $this->schema->getDirectives();
foreach($directives as $directive) {
// Ensure all directives are in fact GraphQL directives.
if (!$directive instanceof Directive) {
$this->reportError(
"Expected directive but got: " . Utils::printSafe($directive) . '.',
is_object($directive) ? $directive->astNode : null
);
continue;
}
// Ensure they are named correctly.
$this->validateName($directive);
// TODO: Ensure proper locations.
$argNames = [];
foreach ($directive->args as $arg) {
$argName = $arg->name;
// Ensure they are named correctly.
$this->validateName($directive);
if (isset($argNames[$argName])) {
$this->reportError(
"Argument @{$directive->name}({$argName}:) can only be defined once.",
$this->getAllDirectiveArgNodes($directive, $argName)
);
continue;
}
$argNames[$argName] = true;
// Ensure the type is an input type.
if (!Type::isInputType($arg->getType())) {
$this->reportError(
"The type of @{$directive->name}({$argName}:) must be Input Type " .
'but got: ' . Utils::printSafe($arg->getType()) . '.',
$this->getDirectiveArgTypeNode($directive, $argName)
);
}
}
}
}
/**
* @param Type|Directive|FieldDefinition|EnumValueDefinition|InputObjectField $node
*/
private function validateName($node)
{
// Ensure names are valid, however introspection types opt out.
$error = Utils::isValidNameError($node->name, $node->astNode);
if ($error && !Introspection::isIntrospectionType($node)) {
$this->addError($error);
}
}
public function validateTypes()
{
$typeMap = $this->schema->getTypeMap();
foreach($typeMap as $typeName => $type) {
// Ensure all provided types are in fact GraphQL type.
if (!$type instanceof NamedType) {
$this->reportError(
"Expected GraphQL named type but got: " . Utils::printSafe($type) . '.',
is_object($type) ? $type->astNode : null
);
continue;
}
$this->validateName($type);
if ($type instanceof ObjectType) {
// Ensure fields are valid
$this->validateFields($type);
// Ensure objects implement the interfaces they claim to.
$this->validateObjectInterfaces($type);
} else if ($type instanceof InterfaceType) {
// Ensure fields are valid.
$this->validateFields($type);
} else if ($type instanceof UnionType) {
// Ensure Unions include valid member types.
$this->validateUnionMembers($type);
} else if ($type instanceof EnumType) {
// Ensure Enums have valid values.
$this->validateEnumValues($type);
} else if ($type instanceof InputObjectType) {
// Ensure Input Object fields are valid.
$this->validateInputFields($type);
}
}
}
/**
* @param ObjectType|InterfaceType $type
*/
private function validateFields($type) {
$fieldMap = $type->getFields();
// Objects and Interfaces both must define one or more fields.
if (!$fieldMap) {
$this->reportError(
"Type {$type->name} must define one or more fields.",
$this->getAllObjectOrInterfaceNodes($type)
);
}
foreach ($fieldMap as $fieldName => $field) {
// Ensure they are named correctly.
$this->validateName($field);
// Ensure they were defined at most once.
$fieldNodes = $this->getAllFieldNodes($type, $fieldName);
if ($fieldNodes && count($fieldNodes) > 1) {
$this->reportError(
"Field {$type->name}.{$fieldName} can only be defined once.",
$fieldNodes
);
continue;
}
// Ensure the type is an output type
if (!Type::isOutputType($field->getType())) {
$this->reportError(
"The type of {$type->name}.{$fieldName} must be Output Type " .
'but got: ' . Utils::printSafe($field->getType()) . '.',
$this->getFieldTypeNode($type, $fieldName)
);
}
// Ensure the arguments are valid
$argNames = [];
foreach($field->args as $arg) {
$argName = $arg->name;
// Ensure they are named correctly.
$this->validateName($arg);
if (isset($argNames[$argName])) {
$this->reportError(
"Field argument {$type->name}.{$fieldName}({$argName}:) can only " .
'be defined once.',
$this->getAllFieldArgNodes($type, $fieldName, $argName)
);
}
$argNames[$argName] = true;
// Ensure the type is an input type
if (!Type::isInputType($arg->getType())) {
$this->reportError(
"The type of {$type->name}.{$fieldName}({$argName}:) must be Input " .
'Type but got: '. Utils::printSafe($arg->getType()) . '.',
$this->getFieldArgTypeNode($type, $fieldName, $argName)
);
}
}
}
}
private function validateObjectInterfaces(ObjectType $object) {
$implementedTypeNames = [];
foreach($object->getInterfaces() as $iface) {
if (isset($implementedTypeNames[$iface->name])) {
$this->reportError(
"Type {$object->name} can only implement {$iface->name} once.",
$this->getAllImplementsInterfaceNodes($object, $iface)
);
continue;
}
$implementedTypeNames[$iface->name] = true;
$this->validateObjectImplementsInterface($object, $iface);
}
}
/**
* @param ObjectType $object
* @param InterfaceType $iface
*/
private function validateObjectImplementsInterface(ObjectType $object, $iface)
{
if (!$iface instanceof InterfaceType) {
$this->reportError(
"Type {$object->name} must only implement Interface types, " .
"it cannot implement ". Utils::printSafe($iface) . ".",
$this->getImplementsInterfaceNode($object, $iface)
);
return;
}
$objectFieldMap = $object->getFields();
$ifaceFieldMap = $iface->getFields();
// Assert each interface field is implemented.
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
$objectField = array_key_exists($fieldName, $objectFieldMap)
? $objectFieldMap[$fieldName]
: null;
// Assert interface field exists on object.
if (!$objectField) {
$this->reportError(
"Interface field {$iface->name}.{$fieldName} expected but " .
"{$object->name} does not provide it.",
[$this->getFieldNode($iface, $fieldName), $object->astNode]
);
continue;
}
// Assert interface field type is satisfied by object field type, by being
// a valid subtype. (covariant)
if (
!TypeComparators::isTypeSubTypeOf(
$this->schema,
$objectField->getType(),
$ifaceField->getType()
)
) {
$this->reportError(
"Interface field {$iface->name}.{$fieldName} expects type ".
"{$ifaceField->getType()} but {$object->name}.{$fieldName} " .
"is type " . Utils::printSafe($objectField->getType()) . ".",
[
$this->getFieldTypeNode($iface, $fieldName),
$this->getFieldTypeNode($object, $fieldName),
]
);
}
// Assert each interface field arg is implemented.
foreach($ifaceField->args as $ifaceArg) {
$argName = $ifaceArg->name;
$objectArg = null;
foreach($objectField->args as $arg) {
if ($arg->name === $argName) {
$objectArg = $arg;
break;
}
}
// Assert interface field arg exists on object field.
if (!$objectArg) {
$this->reportError(
"Interface field argument {$iface->name}.{$fieldName}({$argName}:) " .
"expected but {$object->name}.{$fieldName} does not provide it.",
[
$this->getFieldArgNode($iface, $fieldName, $argName),
$this->getFieldNode($object, $fieldName),
]
);
continue;
}
// Assert interface field arg type matches object field arg type.
// (invariant)
// TODO: change to contravariant?
if (!TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType())) {
$this->reportError(
"Interface field argument {$iface->name}.{$fieldName}({$argName}:) ".
"expects type " . Utils::printSafe($ifaceArg->getType()) . " but " .
"{$object->name}.{$fieldName}({$argName}:) is type " .
Utils::printSafe($objectArg->getType()) . ".",
[
$this->getFieldArgTypeNode($iface, $fieldName, $argName),
$this->getFieldArgTypeNode($object, $fieldName, $argName),
]
);
}
// TODO: validate default values?
}
// Assert additional arguments must not be required.
foreach($objectField->args as $objectArg) {
$argName = $objectArg->name;
$ifaceArg = null;
foreach($ifaceField->args as $arg) {
if ($arg->name === $argName) {
$ifaceArg = $arg;
break;
}
}
if (!$ifaceArg && $objectArg->getType() instanceof NonNull) {
$this->reportError(
"Object field argument {$object->name}.{$fieldName}({$argName}:) " .
"is of required type " . Utils::printSafe($objectArg->getType()) . " but is not also " .
"provided by the Interface field {$iface->name}.{$fieldName}.",
[
$this->getFieldArgTypeNode($object, $fieldName, $argName),
$this->getFieldNode($iface, $fieldName),
]
);
}
}
}
}
private function validateUnionMembers(UnionType $union)
{
$memberTypes = $union->getTypes();
if (!$memberTypes) {
$this->reportError(
"Union type {$union->name} must define one or more member types.",
$union->astNode
);
}
$includedTypeNames = [];
foreach($memberTypes as $memberType) {
if (isset($includedTypeNames[$memberType->name])) {
$this->reportError(
"Union type {$union->name} can only include type ".
"{$memberType->name} once.",
$this->getUnionMemberTypeNodes($union, $memberType->name)
);
continue;
}
$includedTypeNames[$memberType->name] = true;
if (!$memberType instanceof ObjectType) {
$this->reportError(
"Union type {$union->name} can only include Object types, ".
"it cannot include " . Utils::printSafe($memberType) . ".",
$this->getUnionMemberTypeNodes($union, Utils::printSafe($memberType))
);
}
}
}
private function validateEnumValues(EnumType $enumType)
{
$enumValues = $enumType->getValues();
if (!$enumValues) {
$this->reportError(
"Enum type {$enumType->name} must define one or more values.",
$enumType->astNode
);
}
foreach($enumValues as $enumValue) {
$valueName = $enumValue->name;
// Ensure no duplicates
$allNodes = $this->getEnumValueNodes($enumType, $valueName);
if ($allNodes && count($allNodes) > 1) {
$this->reportError(
"Enum type {$enumType->name} can include value {$valueName} only once.",
$allNodes
);
}
// Ensure valid name.
$this->validateName($enumValue);
if ($valueName === 'true' || $valueName === 'false' || $valueName === 'null') {
$this->reportError(
"Enum type {$enumType->name} cannot include value: {$valueName}.",
$enumValue->astNode
);
}
}
}
private function validateInputFields(InputObjectType $inputObj)
{
$fieldMap = $inputObj->getFields();
if (!$fieldMap) {
$this->reportError(
"Input Object type {$inputObj->name} must define one or more fields.",
$inputObj->astNode
);
}
// Ensure the arguments are valid
foreach ($fieldMap as $fieldName => $field) {
// Ensure they are named correctly.
$this->validateName($field);
// TODO: Ensure they are unique per field.
// Ensure the type is an input type
if (!Type::isInputType($field->getType())) {
$this->reportError(
"The type of {$inputObj->name}.{$fieldName} must be Input Type " .
"but got: " . Utils::printSafe($field->getType()) . ".",
$field->astNode ? $field->astNode->type : null
);
}
}
}
/**
* @param ObjectType|InterfaceType $type
* @return ObjectTypeDefinitionNode[]|ObjectTypeExtensionNode[]|InterfaceTypeDefinitionNode[]|InterfaceTypeExtensionNode[]
*/
private function getAllObjectOrInterfaceNodes($type)
{
return $type->astNode
? ($type->extensionASTNodes
? array_merge([$type->astNode], $type->extensionASTNodes)
: [$type->astNode])
: ($type->extensionASTNodes ?: []);
}
/**
* @param ObjectType $type
* @param InterfaceType $iface
* @return NamedTypeNode|null
*/
private function getImplementsInterfaceNode(ObjectType $type, $iface)
{
$nodes = $this->getAllImplementsInterfaceNodes($type, $iface);
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
}
/**
* @param ObjectType $type
* @param InterfaceType $iface
* @return NamedTypeNode[]
*/
private function getAllImplementsInterfaceNodes(ObjectType $type, $iface)
{
$implementsNodes = [];
$astNodes = $this->getAllObjectOrInterfaceNodes($type);
foreach($astNodes as $astNode) {
if ($astNode && $astNode->interfaces) {
foreach($astNode->interfaces as $node) {
if ($node->name->value === $iface->name) {
$implementsNodes[] = $node;
}
}
}
}
return $implementsNodes;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @return FieldDefinitionNode|null
*/
private function getFieldNode($type, $fieldName)
{
$nodes = $this->getAllFieldNodes($type, $fieldName);
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @return FieldDefinitionNode[]
*/
private function getAllFieldNodes($type, $fieldName)
{
$fieldNodes = [];
$astNodes = $this->getAllObjectOrInterfaceNodes($type);
foreach($astNodes as $astNode) {
if ($astNode && $astNode->fields) {
foreach($astNode->fields as $node) {
if ($node->name->value === $fieldName) {
$fieldNodes[] = $node;
}
}
}
}
return $fieldNodes;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @return TypeNode|null
*/
private function getFieldTypeNode($type, $fieldName)
{
$fieldNode = $this->getFieldNode($type, $fieldName);
return $fieldNode ? $fieldNode->type : null;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @param string $argName
* @return InputValueDefinitionNode|null
*/
private function getFieldArgNode($type, $fieldName, $argName)
{
$nodes = $this->getAllFieldArgNodes($type, $fieldName, $argName);
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @param string $argName
* @return InputValueDefinitionNode[]
*/
private function getAllFieldArgNodes($type, $fieldName, $argName)
{
$argNodes = [];
$fieldNode = $this->getFieldNode($type, $fieldName);
if ($fieldNode && $fieldNode->arguments) {
foreach ($fieldNode->arguments as $node) {
if ($node->name->value === $argName) {
$argNodes[] = $node;
}
}
}
return $argNodes;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @param string $argName
* @return TypeNode|null
*/
private function getFieldArgTypeNode($type, $fieldName, $argName)
{
$fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName);
return $fieldArgNode ? $fieldArgNode->type : null;
}
/**
* @param Directive $directive
* @param string $argName
* @return InputValueDefinitionNode[]
*/
private function getAllDirectiveArgNodes(Directive $directive, $argName)
{
$argNodes = [];
$directiveNode = $directive->astNode;
if ($directiveNode && $directiveNode->arguments) {
foreach($directiveNode->arguments as $node) {
if ($node->name->value === $argName) {
$argNodes[] = $node;
}
}
}
return $argNodes;
}
/**
* @param Directive $directive
* @param string $argName
* @return TypeNode|null
*/
private function getDirectiveArgTypeNode(Directive $directive, $argName)
{
$argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0];
return $argNode ? $argNode->type : null;
}
/**
* @param UnionType $union
* @param string $typeName
* @return NamedTypeNode[]
*/
private function getUnionMemberTypeNodes(UnionType $union, $typeName)
{
if ($union->astNode && $union->astNode->types) {
return array_filter(
$union->astNode->types,
function (NamedTypeNode $value) use ($typeName) {
return $value->name->value === $typeName;
}
);
}
return $union->astNode ?
$union->astNode->types : null;
}
/**
* @param EnumType $enum
* @param string $valueName
* @return EnumValueDefinitionNode[]
*/
private function getEnumValueNodes(EnumType $enum, $valueName)
{
if ($enum->astNode && $enum->astNode->values) {
return array_filter(
iterator_to_array($enum->astNode->values),
function (EnumValueDefinitionNode $value) use ($valueName) {
return $value->name->value === $valueName;
}
);
}
return $enum->astNode ?
$enum->astNode->values : null;
}
/** /**
* @param string $message * @param string $message
* @param array|Node|TypeNode|TypeDefinitionNode $nodes * @param array|Node|TypeNode|TypeDefinitionNode $nodes
*/ */
private function reportError($message, $nodes = null) { private function reportError($message, $nodes = null) {
$nodes = array_filter($nodes && is_array($nodes) ? $nodes : [$nodes]); $nodes = array_filter($nodes && is_array($nodes) ? $nodes : [$nodes]);
$this->errors[] = new Error($message, $nodes); $this->addError(new Error($message, $nodes));
}
/**
* @param Error $error
*/
private function addError($error) {
$this->errors[] = $error;
} }
} }

View File

@ -22,7 +22,6 @@ use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\OutputType;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldArgument;
@ -128,53 +127,7 @@ class ASTDefinitionBuilder
/** /**
* @param TypeNode $typeNode * @param TypeNode $typeNode
* @return InputType|Type * @return Type|InputType
* @throws Error
*/
public function buildInputType(TypeNode $typeNode)
{
$type = $this->internalBuildWrappedType($typeNode);
Utils::invariant(Type::isInputType($type), 'Expected Input type.');
return $type;
}
/**
* @param TypeNode $typeNode
* @return OutputType|Type
* @throws Error
*/
public function buildOutputType(TypeNode $typeNode)
{
$type = $this->internalBuildWrappedType($typeNode);
Utils::invariant(Type::isOutputType($type), 'Expected Output type.');
return $type;
}
/**
* @param TypeNode|string $typeNode
* @return ObjectType|Type
* @throws Error
*/
public function buildObjectType($typeNode)
{
$type = $this->buildType($typeNode);
return ObjectType::assertObjectType($type);
}
/**
* @param TypeNode|string $typeNode
* @return InterfaceType|Type
* @throws Error
*/
public function buildInterfaceType($typeNode)
{
$type = $this->buildType($typeNode);
return InterfaceType::assertInterfaceType($type);
}
/**
* @param TypeNode $typeNode
* @return Type
* @throws Error * @throws Error
*/ */
private function internalBuildWrappedType(TypeNode $typeNode) private function internalBuildWrappedType(TypeNode $typeNode)
@ -199,7 +152,10 @@ class ASTDefinitionBuilder
public function buildField(FieldDefinitionNode $field) public function buildField(FieldDefinitionNode $field)
{ {
return [ return [
'type' => $this->buildOutputType($field->type), // Note: While this could make assertions to get the correctly typed
// value, that would throw immediately while type system validation
// with validateSchema() will produce more actionable results.
'type' => $this->internalBuildWrappedType($field->type),
'description' => $this->getDescription($field), 'description' => $this->getDescription($field),
'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null, 'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null,
'deprecationReason' => $this->getDeprecationReason($field), 'deprecationReason' => $this->getDeprecationReason($field),
@ -282,7 +238,10 @@ class ASTDefinitionBuilder
return $value->name->value; return $value->name->value;
}, },
function ($value) { function ($value) {
$type = $this->buildInputType($value->type); // Note: While this could make assertions to get the correctly typed
// value, that would throw immediately while type system validation
// with validateSchema() will produce more actionable results.
$type = $this->internalBuildWrappedType($value->type);
$config = [ $config = [
'name' => $value->name->value, 'name' => $value->name->value,
'type' => $type, 'type' => $type,
@ -339,9 +298,12 @@ class ASTDefinitionBuilder
return new UnionType([ return new UnionType([
'name' => $def->name->value, 'name' => $def->name->value,
'description' => $this->getDescription($def), 'description' => $this->getDescription($def),
// Note: While this could make assertions to get the correctly typed
// values below, that would throw immediately while type system
// validation with validateSchema() will produce more actionable results.
'types' => $def->types 'types' => $def->types
? Utils::map($def->types, function ($typeNode) { ? Utils::map($def->types, function ($typeNode) {
return $this->buildObjectType($typeNode); return $this->buildType($typeNode);
}): }):
[], [],
'astNode' => $def, 'astNode' => $def,
@ -409,7 +371,7 @@ class ASTDefinitionBuilder
{ {
$loc = $node->loc; $loc = $node->loc;
if (!$loc || !$loc->startToken) { if (!$loc || !$loc->startToken) {
return; return null;
} }
$comments = []; $comments = [];
$token = $loc->startToken->prev; $token = $loc->startToken->prev;

View File

@ -221,7 +221,7 @@ class SchemaPrinter
private static function printArgs($options, $args, $indentation = '') private static function printArgs($options, $args, $indentation = '')
{ {
if (count($args) === 0) { if (!$args) {
return ''; return '';
} }

View File

@ -1,8 +1,10 @@
<?php <?php
namespace GraphQL\Utils; namespace GraphQL\Utils;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Error\Warning; use GraphQL\Error\Warning;
use GraphQL\Language\AST\Node;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Definition\WrappingType;
use \Traversable, \InvalidArgumentException; use \Traversable, \InvalidArgumentException;
@ -229,7 +231,7 @@ class Utils
* @param string $message * @param string $message
* @param mixed $sprintfParam1 * @param mixed $sprintfParam1
* @param mixed $sprintfParam2 ... * @param mixed $sprintfParam2 ...
* @throws InvariantViolation * @throws Error
*/ */
public static function invariant($test, $message = '') public static function invariant($test, $message = '')
{ {
@ -239,6 +241,7 @@ class Utils
array_shift($args); array_shift($args);
$message = call_user_func_array('sprintf', $args); $message = call_user_func_array('sprintf', $args);
} }
// TODO switch to Error here
throw new InvariantViolation($message); throw new InvariantViolation($message);
} }
} }
@ -302,8 +305,12 @@ class Utils
return $var->toString(); return $var->toString();
} }
if (is_object($var)) { if (is_object($var)) {
if (method_exists($var, '__toString')) {
return (string) $var;
} else {
return 'instance of ' . get_class($var); return 'instance of ' . get_class($var);
} }
}
if (is_array($var)) { if (is_array($var)) {
return json_encode($var); return json_encode($var);
} }
@ -399,34 +406,46 @@ class Utils
} }
/** /**
* Upholds the spec rules about naming.
*
* @param $name * @param $name
* @param bool $isIntrospection * @throws Error
* @throws InvariantViolation
*/ */
public static function assertValidName($name, $isIntrospection = false) public static function assertValidName($name)
{ {
$regex = '/^[_a-zA-Z][_a-zA-Z0-9]*$/'; $error = self::isValidNameError($name);
if ($error) {
throw $error;
}
}
if (!$name || !is_string($name)) { /**
throw new InvariantViolation( * Returns an Error if a name is invalid.
"Must be named. Unexpected name: " . self::printSafe($name) *
* @param string $name
* @param Node|null $node
* @return Error|null
*/
public static function isValidNameError($name, $node = null)
{
Utils::invariant(is_string($name), 'Expected string');
if (isset($name[1]) && $name[0] === '_' && $name[1] === '_') {
return new Error(
"Name \"{$name}\" must not begin with \"__\", which is reserved by " .
"GraphQL introspection.",
$node
); );
} }
if (!$isIntrospection && isset($name[1]) && $name[0] === '_' && $name[1] === '_') { if (!preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name)) {
Warning::warnOnce( return new Error(
'Name "'.$name.'" must not begin with "__", which is reserved by ' . "Names must match /^[_a-zA-Z][_a-zA-Z0-9]*\$/ but \"{$name}\" does not.",
'GraphQL introspection. In a future release of graphql this will ' . $node
'become an exception',
Warning::WARNING_NAME
); );
} }
if (!preg_match($regex, $name)) { return null;
throw new InvariantViolation(
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "'.$name.'" does not.'
);
}
} }
/** /**

View File

@ -468,37 +468,6 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
} }
} }
/**
* @it prohibits putting non-Object types in unions
*/
public function testProhibitsPuttingNonObjectTypesInUnions()
{
$int = Type::int();
$badUnionTypes = [
$int,
new NonNull($int),
new ListOfType($int),
$this->interfaceType,
$this->unionType,
$this->enumType,
$this->inputObjectType
];
foreach ($badUnionTypes as $type) {
try {
$union = new UnionType(['name' => 'BadUnion', 'types' => [$type]]);
$union->assertValid();
$this->fail('Expected exception not thrown');
} catch (\Exception $e) {
$this->assertSame(
'BadUnion may only contain Object types, it cannot contain: ' . Utils::printSafe($type) . '.',
$e->getMessage()
);
}
}
}
/** /**
* @it allows a thunk for Union\'s types * @it allows a thunk for Union\'s types
*/ */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
<?php
namespace GraphQL\Tests\Utils;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils;
class AssertValidNameTest extends \PHPUnit_Framework_TestCase
{
// Describe: assertValidName()
/**
* @it throws for use of leading double underscores
*/
public function testThrowsForUseOfLeadingDoubleUnderscores()
{
$this->setExpectedException(
Error::class,
'"__bad" must not begin with "__", which is reserved by GraphQL introspection.'
);
Utils::assertValidName('__bad');
}
/**
* @it throws for non-strings
*/
public function testThrowsForNonStrings()
{
$this->setExpectedException(
InvariantViolation::class,
'Expected string'
);
Utils::assertValidName([]);
}
/**
* @it throws for names with invalid characters
*/
public function testThrowsForNamesWithInvalidCharacters()
{
$this->setExpectedException(
Error::class,
'Names must match'
);
Utils::assertValidName('>--()-->');
}
}