diff --git a/docs/reference.md b/docs/reference.md index 789eb5f..7ee6f21 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -231,6 +231,15 @@ static function isCompositeType($type) static function isAbstractType($type) ``` +```php +/** + * @api + * @param Type $type + * @return bool + */ +static function isType($type) +``` + ```php /** * @api @@ -431,7 +440,7 @@ static function create(array $options = []) * @param ObjectType $query * @return SchemaConfig */ -function setQuery(GraphQL\Type\Definition\ObjectType $query) +function setQuery($query) ``` ```php @@ -440,7 +449,7 @@ function setQuery(GraphQL\Type\Definition\ObjectType $query) * @param ObjectType $mutation * @return SchemaConfig */ -function setMutation(GraphQL\Type\Definition\ObjectType $mutation) +function setMutation($mutation) ``` ```php @@ -449,7 +458,7 @@ function setMutation(GraphQL\Type\Definition\ObjectType $mutation) * @param ObjectType $subscription * @return SchemaConfig */ -function setSubscription(GraphQL\Type\Definition\ObjectType $subscription) +function setSubscription($subscription) ``` ```php @@ -670,6 +679,18 @@ function getDirectives() function getDirective($name) ``` +```php +/** + * Validates schema. + * + * This operation requires full schema scan. Do not use in production environment. + * + * @api + * @return InvariantViolation[]|Error[] + */ +function validate() +``` + ```php /** * Validates schema. diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index fab555a..8ce9baa 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -333,7 +333,6 @@ class Executor } } - /** * Extracts the root type of the operation from the schema. * @@ -346,12 +345,19 @@ class Executor { switch ($operation->operation) { case 'query': - return $schema->getQueryType(); + $queryType = $schema->getQueryType(); + if (!$queryType) { + throw new Error( + 'Schema does not define the required query root type.', + [$operation] + ); + } + return $queryType; case 'mutation': $mutationType = $schema->getMutationType(); if (!$mutationType) { throw new Error( - 'Schema is not configured for mutations', + 'Schema is not configured for mutations.', [$operation] ); } @@ -360,14 +366,14 @@ class Executor $subscriptionType = $schema->getSubscriptionType(); if (!$subscriptionType) { throw new Error( - 'Schema is not configured for subscriptions', + 'Schema is not configured for subscriptions.', [ $operation ] ); } return $subscriptionType; default: throw new Error( - 'Can only execute queries, mutations and subscriptions', + 'Can only execute queries, mutations and subscriptions.', [$operation] ); } diff --git a/src/Server.php b/src/Server.php index 3350b1f..16d1f05 100644 --- a/src/Server.php +++ b/src/Server.php @@ -472,6 +472,7 @@ class Server { try { $schema = $this->getSchema(); + $schema->assertValid(); } catch (InvariantViolation $e) { throw new InvariantViolation("Cannot validate, schema contains errors: {$e->getMessage()}", null, $e); } diff --git a/src/Type/Definition/InterfaceType.php b/src/Type/Definition/InterfaceType.php index 9d532ec..a92be48 100644 --- a/src/Type/Definition/InterfaceType.php +++ b/src/Type/Definition/InterfaceType.php @@ -3,6 +3,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\InterfaceTypeDefinitionNode; +use GraphQL\Language\AST\InterfaceTypeExtensionNode; use GraphQL\Utils\Utils; /** @@ -21,6 +22,11 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT */ public $astNode; + /** + * @var InterfaceTypeExtensionNode[] + */ + public $extensionASTNodes; + /** * InterfaceType constructor. * @param array $config @@ -46,6 +52,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT $this->name = $config['name']; $this->description = isset($config['description']) ? $config['description'] : null; $this->astNode = isset($config['astNode']) ? $config['astNode'] : null; + $this->extensionASTNodes = isset($config['extensionASTNodes']) ? $config['extensionASTNodes'] : null; $this->config = $config; } diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index 717942f..5f2d3e1 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -152,13 +152,13 @@ class ObjectType extends Type implements OutputType, CompositeType $interfaces = isset($this->config['interfaces']) ? $this->config['interfaces'] : []; $interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces; - if (!is_array($interfaces)) { + if ($interfaces && !is_array($interfaces)) { throw new InvariantViolation( "{$this->name} interfaces must be an Array or a callable which returns an Array." ); } - $this->interfaces = $interfaces; + $this->interfaces = $interfaces ?: []; } return $this->interfaces; } @@ -227,19 +227,5 @@ class ObjectType extends Type implements OutputType, CompositeType $arg->assertValid($field, $this); } } - - $implemented = []; - foreach ($this->getInterfaces() as $iface) { - Utils::invariant( - $iface instanceof InterfaceType, - "{$this->name} may only implement Interface types, it cannot implement %s.", - Utils::printSafe($iface) - ); - Utils::invariant( - !isset($implemented[$iface->name]), - "{$this->name} may declare it implements {$iface->name} only once." - ); - $implemented[$iface->name] = true; - } } } diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 89bac9b..5afceb8 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -2,6 +2,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Error\InvariantViolation; +use GraphQL\Language\AST\ListType; use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\TypeDefinitionNode; use GraphQL\Type\Introspection; @@ -203,6 +204,25 @@ abstract class Type implements \JsonSerializable return $type instanceof AbstractType; } + /** + * @api + * @param Type $type + * @return bool + */ + public static function isType($type) + { + return ( + $type instanceof ScalarType || + $type instanceof ObjectType || + $type instanceof InterfaceType || + $type instanceof UnionType || + $type instanceof EnumType || + $type instanceof InputObjectType || + $type instanceof ListType || + $type instanceof NonNull + ); + } + /** * @api * @param Type $type diff --git a/src/Type/Schema.php b/src/Type/Schema.php index b4ec795..b68ef12 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -1,18 +1,16 @@ $subscriptionType ]; } + if (is_array($config)) { $config = SchemaConfig::create($config); } - Utils::invariant( - $config instanceof SchemaConfig, - 'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s', - implode(', ', [ - 'query', - 'mutation', - 'subscription', - 'types', - 'directives', - 'typeLoader' - ]), - Utils::getVariableType($config) - ); - - Utils::invariant( - $config->query instanceof ObjectType, - "Schema query must be Object Type but got: " . Utils::getVariableType($config->query) - ); + // If this schema was built from a source known to be valid, then it may be + // marked with assumeValid to avoid an additional type system validation. + if ($config->getAssumeValid()) { + $this->validationErrors = []; + } else { + // Otherwise check for common mistakes during construction to produce + // clear and early error messages. + Utils::invariant( + $config instanceof SchemaConfig, + 'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s', + implode(', ', [ + 'query', + 'mutation', + 'subscription', + 'types', + 'directives', + 'typeLoader' + ]), + Utils::getVariableType($config) + ); + Utils::invariant( + !$config->types || is_array($config->types) || is_callable($config->types), + "\"types\" must be array or callable if provided but got: " . Utils::getVariableType($config->types) + ); + Utils::invariant( + !$config->directives || is_array($config->directives), + "\"directives\" must be Array if provided but got: " . Utils::getVariableType($config->directives) + ); + } $this->config = $config; - $this->resolvedTypes[$config->query->name] = $config->query; - + if ($config->query) { + $this->resolvedTypes[$config->query->name] = $config->query; + } if ($config->mutation) { $this->resolvedTypes[$config->mutation->name] = $config->mutation; } if ($config->subscription) { $this->resolvedTypes[$config->subscription->name] = $config->subscription; } - if (is_array($this->config->types)) { + if ($this->config->types) { foreach ($this->resolveAdditionalTypes() as $type) { if (isset($this->resolvedTypes[$type->name])) { Utils::invariant( @@ -393,6 +409,32 @@ class Schema return isset($typeMap[$typeName]) ? $typeMap[$typeName] : null; } + /** + * Validates schema. + * + * This operation requires full schema scan. Do not use in production environment. + * + * @api + * @return InvariantViolation[]|Error[] + */ + public function validate() { + // If this Schema has already been validated, return the previous results. + if ($this->validationErrors !== null) { + return $this->validationErrors; + } + // Validate the schema, producing a list of errors. + $context = new SchemaValidationContext($this); + $context->validateRootTypes(); + $context->validateDirectives(); + $context->validateTypes(); + + // Persist the results of validation before returning to ensure validation + // does not run multiple times for this schema. + $this->validationErrors = $context->getErrors(); + + return $this->validationErrors; + } + /** * Validates schema. * @@ -403,18 +445,13 @@ class Schema */ public function assertValid() { - foreach ($this->config->getDirectives() as $index => $directive) { - Utils::invariant( - $directive instanceof Directive, - "Each entry of \"directives\" option of Schema config must be an instance of %s but entry at position %d is %s.", - Directive::class, - $index, - Utils::printSafe($directive) - ); + $errors = $this->validate(); + + if ($errors) { + throw new InvariantViolation(implode("\n\n", $this->validationErrors)); } $internalTypes = Type::getInternalTypes() + Introspection::getTypes(); - foreach ($this->getTypeMap() as $name => $type) { if (isset($internalTypes[$name])) { continue ; @@ -422,22 +459,6 @@ class Schema $type->assertValid(); - if ($type instanceof AbstractType) { - $possibleTypes = $this->getPossibleTypes($type); - - Utils::invariant( - !empty($possibleTypes), - "Could not find possible implementing types for {$type->name} " . - 'in schema. Check that schema.types is defined and is an array of ' . - 'all possible types in the schema.' - ); - - } else if ($type instanceof ObjectType) { - foreach ($type->getInterfaces() as $iface) { - $this->assertImplementsIntarface($type, $iface); - } - } - // Make sure type loader returns the same instance as registered in other places of schema if ($this->config->typeLoader) { Utils::invariant( @@ -448,74 +469,4 @@ class Schema } } } - - private function assertImplementsIntarface(ObjectType $object, InterfaceType $iface) - { - $objectFieldMap = $object->getFields(); - $ifaceFieldMap = $iface->getFields(); - - // Assert each interface field is implemented. - foreach ($ifaceFieldMap as $fieldName => $ifaceField) { - - // Assert interface field exists on object. - Utils::invariant( - isset($objectFieldMap[$fieldName]), - "{$iface->name} expects field \"{$fieldName}\" but {$object->name} does not provide it" - ); - - $objectField = $objectFieldMap[$fieldName]; - - // Assert interface field type is satisfied by object field type, by being - // a valid subtype. (covariant) - Utils::invariant( - TypeComparators::isTypeSubTypeOf($this, $objectField->getType(), $ifaceField->getType()), - "{$iface->name}.{$fieldName} expects type \"{$ifaceField->getType()}\" " . - "but " . - "{$object->name}.${fieldName} provides type \"{$objectField->getType()}\"" - ); - - // Assert each interface field arg is implemented. - foreach ($ifaceField->args as $ifaceArg) { - $argName = $ifaceArg->name; - - /** @var FieldArgument $objectArg */ - $objectArg = Utils::find($objectField->args, function(FieldArgument $arg) use ($argName) { - return $arg->name === $argName; - }); - - // Assert interface field arg exists on object field. - Utils::invariant( - $objectArg, - "{$iface->name}.{$fieldName} expects argument \"{$argName}\" but ". - "{$object->name}.{$fieldName} does not provide it." - ); - - // Assert interface field arg type matches object field arg type. - // (invariant) - Utils::invariant( - TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType()), - "{$iface->name}.{$fieldName}({$argName}:) expects type " . - "\"{$ifaceArg->getType()->name}\" but " . - "{$object->name}.{$fieldName}({$argName}:) provides type " . - "\"{$objectArg->getType()->name}\"." - ); - - // Assert additional arguments must not be required. - foreach ($objectField->args as $objectArg) { - $argName = $objectArg->name; - $ifaceArg = Utils::find($ifaceField->args, function(FieldArgument $arg) use ($argName) { - return $arg->name === $argName; - }); - if (!$ifaceArg) { - Utils::invariant( - !($objectArg->getType() instanceof NonNull), - "{$object->name}.{$fieldName}({$argName}:) is of required type " . - "\"{$objectArg->getType()}\" but is not also provided by the " . - "interface {$iface->name}.{$fieldName}." - ); - } - } - } - } - } } diff --git a/src/Type/SchemaConfig.php b/src/Type/SchemaConfig.php index 2b03c37..5905a94 100644 --- a/src/Type/SchemaConfig.php +++ b/src/Type/SchemaConfig.php @@ -58,6 +58,11 @@ class SchemaConfig */ public $astNode; + /** + * @var bool + */ + public $assumeValid; + /** * Converts an array of options to instance of SchemaConfig * (or just returns empty config when array is not passed). @@ -72,47 +77,22 @@ class SchemaConfig if (!empty($options)) { if (isset($options['query'])) { - Utils::invariant( - $options['query'] instanceof ObjectType, - 'Schema query must be Object Type if provided but got: %s', - Utils::printSafe($options['query']) - ); $config->setQuery($options['query']); } if (isset($options['mutation'])) { - Utils::invariant( - $options['mutation'] instanceof ObjectType, - 'Schema mutation must be Object Type if provided but got: %s', - Utils::printSafe($options['mutation']) - ); $config->setMutation($options['mutation']); } if (isset($options['subscription'])) { - Utils::invariant( - $options['subscription'] instanceof ObjectType, - 'Schema subscription must be Object Type if provided but got: %s', - Utils::printSafe($options['subscription']) - ); $config->setSubscription($options['subscription']); } if (isset($options['types'])) { - Utils::invariant( - is_array($options['types']) || is_callable($options['types']), - 'Schema types must be array or callable if provided but got: %s', - Utils::printSafe($options['types']) - ); $config->setTypes($options['types']); } if (isset($options['directives'])) { - Utils::invariant( - is_array($options['directives']), - 'Schema directives must be array if provided but got: %s', - Utils::printSafe($options['directives']) - ); $config->setDirectives($options['directives']); } @@ -140,13 +120,12 @@ class SchemaConfig } if (isset($options['astNode'])) { - Utils::invariant( - $options['astNode'] instanceof SchemaDefinitionNode, - 'Schema astNode must be an instance of SchemaDefinitionNode but got: %s', - Utils::printSafe($options['typeLoader']) - ); $config->setAstNode($options['astNode']); } + + if (isset($options['assumeValid'])) { + $config->setAssumeValid((bool) $options['assumeValid']); + } } return $config; @@ -175,7 +154,7 @@ class SchemaConfig * @param ObjectType $query * @return SchemaConfig */ - public function setQuery(ObjectType $query) + public function setQuery($query) { $this->query = $query; return $this; @@ -186,7 +165,7 @@ class SchemaConfig * @param ObjectType $mutation * @return SchemaConfig */ - public function setMutation(ObjectType $mutation) + public function setMutation($mutation) { $this->mutation = $mutation; return $this; @@ -197,7 +176,7 @@ class SchemaConfig * @param ObjectType $subscription * @return SchemaConfig */ - public function setSubscription(ObjectType $subscription) + public function setSubscription($subscription) { $this->subscription = $subscription; return $this; @@ -236,6 +215,16 @@ class SchemaConfig return $this; } + /** + * @param bool $assumeValid + * @return SchemaConfig + */ + public function setAssumeValid($assumeValid) + { + $this->assumeValid = $assumeValid; + return $this; + } + /** * @api * @return ObjectType @@ -289,4 +278,12 @@ class SchemaConfig { return $this->typeLoader; } + + /** + * @return bool + */ + public function getAssumeValid() + { + return $this->assumeValid; + } } diff --git a/src/Type/SchemaValidationContext.php b/src/Type/SchemaValidationContext.php new file mode 100644 index 0000000..a0a4312 --- /dev/null +++ b/src/Type/SchemaValidationContext.php @@ -0,0 +1,384 @@ +schema = $schema; + } + + /** + * @return Error[] + */ + public function getErrors() { + return $this->errors; + } + + public function validateRootTypes() { + $queryType = $this->schema->getQueryType(); + if (!$queryType) { + $this->reportError( + 'Query root type must be provided.', + $this->schema->getAstNode() + ); + } else if (!$queryType instanceof ObjectType) { + $this->reportError( + 'Query root type must be Object type but got: ' . Utils::getVariableType($queryType) . '.', + $this->getOperationTypeNode($queryType, 'query') + ); + } + + $mutationType = $this->schema->getMutationType(); + if ($mutationType && !$mutationType instanceof ObjectType) { + $this->reportError( + 'Mutation root type must be Object type if provided but got: ' . Utils::getVariableType($mutationType) . '.', + $this->getOperationTypeNode($mutationType, 'mutation') + ); + } + + $subscriptionType = $this->schema->getSubscriptionType(); + if ($subscriptionType && !$subscriptionType instanceof ObjectType) { + $this->reportError( + 'Subscription root type must be Object type if provided but got: ' . Utils::getVariableType($subscriptionType) . '.', + $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 string $operation + * + * @return TypeNode|TypeDefinitionNode + */ + private function getOperationTypeNode($type, $operation) + { + $astNode = $this->schema->getAstNode(); + + $operationTypeNode = null; + if ($astNode instanceof SchemaDefinitionNode) { + $operationTypeNode = null; + + foreach($astNode->operationTypes as $operationType) { + if ($operationType->operation === $operation) { + $operationTypeNode = $operationType; + break; + } + } + } + + return $operationTypeNode ? $operationTypeNode->type : ($type ? $type->astNode : null); + } + + /** + * @param string $message + * @param array|Node|TypeNode|TypeDefinitionNode $nodes + */ + private function reportError($message, $nodes = null) { + $nodes = array_filter($nodes && is_array($nodes) ? $nodes : [$nodes]); + $this->errors[] = new Error($message, $nodes); + } +} diff --git a/src/Utils.php b/src/Utils.php index b73186f..feabcf0 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -6,6 +6,9 @@ trigger_error( E_USER_DEPRECATED ); +/** + * @deprecated Use GraphQL\Utils\Utils + */ class Utils extends \GraphQL\Utils\Utils { } diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index d38310b..073e773 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -266,9 +266,12 @@ class ASTDefinitionBuilder private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def) { - if (isset($def->interfaces)) { + if ($def->interfaces) { + // Note: While this could make early assertions to get the correctly + // typed values, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. return Utils::map($def->interfaces, function ($iface) { - return $this->buildInterfaceType($iface); + return $this->buildType($iface); }); } return null; diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index 9d764cd..0b8ae31 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -130,20 +130,20 @@ class BuildSchema $directives[] = Directive::deprecatedDirective(); } - if (!isset($operationTypes['query'])) { - throw new Error( - 'Must provide schema definition with query type or a type named Query.' - ); - } + // Note: While this could make early assertions to get the correctly + // typed values below, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. $schema = new Schema([ - 'query' => $defintionBuilder->buildObjectType($operationTypes['query']), - 'mutation' => isset($operationTypes['mutation']) ? - $defintionBuilder->buildObjectType($operationTypes['mutation']) : - null, - 'subscription' => isset($operationTypes['subscription']) ? - $defintionBuilder->buildObjectType($operationTypes['subscription']) : - null, + 'query' => isset($operationTypes['query']) + ? $defintionBuilder->buildType($operationTypes['query']) + : null, + 'mutation' => isset($operationTypes['mutation']) + ? $defintionBuilder->buildType($operationTypes['mutation']) + : null, + 'subscription' => isset($operationTypes['subscription']) + ? $defintionBuilder->buildType($operationTypes['subscription']) + : null, 'typeLoader' => function ($name) use ($defintionBuilder) { return $defintionBuilder->buildType($name); }, diff --git a/src/Utils/TypeComparators.php b/src/Utils/TypeComparators.php index 64639b4..0ddcbaf 100644 --- a/src/Utils/TypeComparators.php +++ b/src/Utils/TypeComparators.php @@ -48,7 +48,7 @@ class TypeComparators * @param Type $superType * @return bool */ - static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType) + static function isTypeSubTypeOf(Schema $schema, $maybeSubType, $superType) { // Equivalent type is a valid subtype if ($maybeSubType === $superType) { diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 03c6895..db7df22 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -39,8 +39,9 @@ class ServerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(500, $server->getUnexpectedErrorStatus()); $this->assertEquals(DocumentValidator::allRules(), $server->getValidationRules()); - $this->setExpectedException(InvariantViolation::class, 'Schema query must be Object Type but got: NULL'); - $server->getSchema(); + $schema = $server->getSchema(); + $this->setExpectedException(InvariantViolation::class, 'Query root type must be provided.'); + $schema->assertValid(); } public function testCannotUseSetQueryTypeAndSetSchema() @@ -328,8 +329,8 @@ class ServerTest extends \PHPUnit_Framework_TestCase $this->assertInternalType('array', $errors); $this->assertNotEmpty($errors); - $this->setExpectedException(InvariantViolation::class, 'Cannot validate, schema contains errors: Schema query must be Object Type but got: NULL'); $server = Server::create(); + $this->setExpectedException(InvariantViolation::class, 'Cannot validate, schema contains errors: Query root type must be provided.'); $server->validate($ast); } @@ -538,15 +539,14 @@ class ServerTest extends \PHPUnit_Framework_TestCase { $mock = $this->getMockBuilder('GraphQL\Server') ->setMethods(['readInput', 'produceOutput']) - ->getMock() - ; + ->getMock(); $mock->method('readInput') ->will($this->returnValue(json_encode(['query' => '{err}']))); $output = null; $mock->method('produceOutput') - ->will($this->returnCallback(function($a1, $a2) use (&$output) { + ->will($this->returnCallback(function ($a1, $a2) use (&$output) { $output = func_get_args(); })); @@ -554,17 +554,35 @@ class ServerTest extends \PHPUnit_Framework_TestCase $mock->handleRequest(); $this->assertInternalType('array', $output); - $this->assertArraySubset(['errors' => [['message' => 'Unexpected Error']]], $output[0]); - $this->assertEquals(500, $output[1]); + $this->assertArraySubset(['errors' => [['message' => 'Schema does not define the required query root type.']]], $output[0]); + $this->assertEquals(200, $output[1]); $output = null; $mock->setUnexpectedErrorMessage($newErr = 'Hey! Something went wrong!'); $mock->setUnexpectedErrorStatus(501); + $mock->method('readInput') + ->will($this->throwException(new \Exception('test'))); $mock->handleRequest(); $this->assertInternalType('array', $output); $this->assertEquals(['errors' => [['message' => $newErr]]], $output[0]); $this->assertEquals(501, $output[1]); + } + + public function testHandleRequest2() + { + $mock = $this->getMockBuilder('GraphQL\Server') + ->setMethods(['readInput', 'produceOutput']) + ->getMock(); + + $mock->method('readInput') + ->will($this->returnValue(json_encode(['query' => '{err}']))); + + $output = null; + $mock->method('produceOutput') + ->will($this->returnCallback(function ($a1, $a2) use (&$output) { + $output = func_get_args(); + })); $mock->setQueryType(new ObjectType([ 'name' => 'Query', diff --git a/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index 6d689c6..26626ad 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -1,6 +1,7 @@ getMessage() === $message) { + if ($error instanceof Error) { + $errorLocations = []; + foreach ($error->getLocations() as $location) { + $errorLocations[] = $location->toArray(); + } + $this->assertEquals($locations, $errorLocations ?: null); + } + return; + } + } + + $this->fail( + 'Failed asserting that the array of validation messages contains ' . + 'the message "' . $message . '"' . "\n" . + 'Found the following messages in the array:' . "\n" . + join("\n", array_map(function($error) { return "\"{$error->getMessage()}\""; }, $array)) + ); + } + public function testRejectsTypesWithoutNames() { $this->assertEachCallableThrows([ @@ -213,11 +242,22 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsASchemaWhoseQueryTypeIsAnObjectType() { - // Must not throw: - $schema = new Schema([ - 'query' => $this->SomeObjectType - ]); - $schema->assertValid(); + $schema = BuildSchema::build(' + type Query { + test: String + } + '); + $this->assertEquals([], $schema->validate()); + + $schemaWithDef = BuildSchema::build(' + schema { + query: QueryRoot + } + type QueryRoot { + test: String + } + '); + $this->assertEquals([], $schemaWithDef->validate()); } /** @@ -225,17 +265,32 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsASchemaWhoseQueryAndMutationTypesAreObjectTypes() { - $mutationType = new ObjectType([ - 'name' => 'Mutation', - 'fields' => [ - 'edit' => ['type' => Type::string()] - ] - ]); - $schema = new Schema([ - 'query' => $this->SomeObjectType, - 'mutation' => $mutationType - ]); - $schema->assertValid(); + $schema = BuildSchema::build(' + type Query { + test: String + } + + type Mutation { + test: String + } + '); + $this->assertEquals([], $schema->validate()); + + $schema = BuildSchema::build(' + schema { + query: QueryRoot + mutation: MutationRoot + } + + type QueryRoot { + test: String + } + + type MutationRoot { + test: String + } + '); + $this->assertEquals([], $schema->validate()); } /** @@ -243,17 +298,32 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsASchemaWhoseQueryAndSubscriptionTypesAreObjectTypes() { - $subscriptionType = new ObjectType([ - 'name' => 'Subscription', - 'fields' => [ - 'subscribe' => ['type' => Type::string()] - ] - ]); - $schema = new Schema([ - 'query' => $this->SomeObjectType, - 'subscription' => $subscriptionType - ]); - $schema->assertValid(); + $schema = BuildSchema::build(' + type Query { + test: String + } + + type Subscription { + test: String + } + '); + $this->assertEquals([], $schema->validate()); + + $schema = BuildSchema::build(' + schema { + query: QueryRoot + subscription: SubscriptionRoot + } + + type QueryRoot { + test: String + } + + type SubscriptionRoot { + test: String + } + '); + $this->assertEquals([], $schema->validate()); } /** @@ -261,22 +331,68 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsASchemaWithoutAQueryType() { - $this->setExpectedException(InvariantViolation::class, 'Schema query must be Object Type but got: NULL'); - new Schema([]); + $schema = BuildSchema::build(' + type Mutation { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'Query root type must be provided.' + ); + + + $schemaWithDef = BuildSchema::build(' + schema { + mutation: MutationRoot + } + + type MutationRoot { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schemaWithDef->validate(), + 'Query root type must be provided.', + [['line' => 2, 'column' => 7]] + ); } /** - * @it rejects a Schema whose query type is an input type + * @it rejects a Schema whose query root type is not an Object type */ - public function testRejectsASchemaWhoseQueryTypeIsAnInputType() + public function testRejectsASchemaWhoseQueryTypeIsNotAnObjectType() { - $this->setExpectedException( - InvariantViolation::class, - 'Schema query must be Object Type if provided but got: SomeInputObject' + $schema = BuildSchema::build(' + input Query { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'Query root type must be Object type but got: Query.', + [['line' => 2, 'column' => 7]] + ); + + + $schemaWithDef = BuildSchema::build(' + schema { + query: SomeInputObject + } + + input SomeInputObject { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schemaWithDef->validate(), + 'Query root type must be Object type but got: SomeInputObject.', + [['line' => 3, 'column' => 16]] ); - new Schema([ - 'query' => $this->SomeInputObjectType - ]); } /** @@ -284,14 +400,43 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsASchemaWhoseMutationTypeIsAnInputType() { - $this->setExpectedException( - InvariantViolation::class, - 'Schema mutation must be Object Type if provided but got: SomeInputObject' + $schema = BuildSchema::build(' + type Query { + field: String + } + + input Mutation { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'Mutation root type must be Object type if provided but got: Mutation.', + [['line' => 6, 'column' => 7]] + ); + + + $schemaWithDef = BuildSchema::build(' + schema { + query: Query + mutation: SomeInputObject + } + + type Query { + field: String + } + + input SomeInputObject { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schemaWithDef->validate(), + 'Mutation root type must be Object type if provided but got: SomeInputObject.', + [['line' => 4, 'column' => 19]] ); - new Schema([ - 'query' => $this->SomeObjectType, - 'mutation' => $this->SomeInputObjectType - ]); } /** @@ -299,14 +444,45 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsASchemaWhoseSubscriptionTypeIsAnInputType() { - $this->setExpectedException( - InvariantViolation::class, - 'Schema subscription must be Object Type if provided but got: SomeInputObject' + $schema = BuildSchema::build(' + type Query { + field: String + } + + input Subscription { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'Subscription root type must be Object type if provided but got: Subscription.', + [['line' => 6, 'column' => 7]] ); - new Schema([ - 'query' => $this->SomeObjectType, - 'subscription' => $this->SomeInputObjectType - ]); + + + $schemaWithDef = BuildSchema::build(' + schema { + query: Query + subscription: SomeInputObject + } + + type Query { + field: String + } + + input SomeInputObject { + test: String + } + '); + + $this->assertContainsValidationMessage( + $schemaWithDef->validate(), + 'Subscription root type must be Object type if provided but got: SomeInputObject.', + [['line' => 4, 'column' => 23]] + ); + + } /** @@ -319,12 +495,10 @@ class ValidationTest extends \PHPUnit_Framework_TestCase 'directives' => ['somedirective'] ]); - $this->setExpectedException( - InvariantViolation::class, - 'Each entry of "directives" option of Schema config must be an instance of GraphQL\Type\Definition\Directive but entry at position 0 is "somedirective".' + $this->assertContainsValidationMessage( + $schema->validate(), + 'Expected directive but got: somedirective' ); - - $schema->assertValid(); } // DESCRIBE: Type System: A Schema must contain uniquely named types @@ -774,39 +948,6 @@ class ValidationTest extends \PHPUnit_Framework_TestCase $schema->assertValid(); } - /** - * @it rejects an Object that declare it implements same interface more than once - */ - public function testRejectsAnObjectThatDeclareItImplementsSameInterfaceMoreThanOnce() - { - $NonUniqInterface = new InterfaceType([ - 'name' => 'NonUniqInterface', - 'fields' => ['f' => ['type' => Type::string()]], - ]); - - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => ['f' => ['type' => Type::string()]], - ]); - - $schema = $this->schemaWithFieldType(new ObjectType([ - 'name' => 'SomeObject', - 'interfaces' => function () use ($NonUniqInterface, $AnotherInterface) { - return [$NonUniqInterface, $AnotherInterface, $NonUniqInterface]; - }, - 'fields' => ['f' => ['type' => Type::string()]] - ])); - - $this->setExpectedException( - InvariantViolation::class, - 'SomeObject may declare it implements NonUniqInterface only once.' - ); - - $schema->assertValid(); - } - - // TODO: rejects an Object type with interfaces as a function returning an incorrect type - /** * @it rejects an Object type with interfaces as a function returning an incorrect type */ @@ -1654,52 +1795,6 @@ class ValidationTest extends \PHPUnit_Framework_TestCase $schema->assertValid(); } - - - // DESCRIBE: Type System: Objects can only implement interfaces - - /** - * @it accepts an Object implementing an Interface - */ - public function testAcceptsAnObjectImplementingAnInterface() - { - $AnotherInterfaceType = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => ['f' => ['type' => Type::string()]] - ]); - - $schema = $this->schemaWithObjectImplementingType($AnotherInterfaceType); - $schema->assertValid(); - } - - /** - * @it rejects an Object implementing a non-Interface type - */ - public function testRejectsAnObjectImplementingANonInterfaceType() - { - $notInterfaceTypes = $this->withModifiers([ - $this->SomeScalarType, - $this->SomeEnumType, - $this->SomeObjectType, - $this->SomeUnionType, - $this->SomeInputObjectType, - ]); - foreach ($notInterfaceTypes as $type) { - $schema = $this->schemaWithObjectImplementingType($type); - - try { - $schema->assertValid(); - $this->fail('Exepected exception not thrown for type ' . $type); - } catch (InvariantViolation $e) { - $this->assertEquals( - 'BadObject may only implement Interface types, it cannot implement ' . $type . '.', - $e->getMessage() - ); - } - } - } - - // DESCRIBE: Type System: Unions must represent Object types /** @@ -1991,7 +2086,6 @@ class ValidationTest extends \PHPUnit_Framework_TestCase } } - // DESCRIBE: Objects must adhere to Interface they implement /** @@ -1999,33 +2093,24 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWhichImplementsAnInterface() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()] - ] - ] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } + + interface AnotherInterface { + field(input: String): String + } + + type AnotherObject implements AnotherInterface { + field(input: String): String + } + '); - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()] - ] - ] - ] - ]); - - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); + $this->assertEquals( + [], + $schema->validate() + ); } /** @@ -2033,34 +2118,25 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWhichImplementsAnInterfaceAlongWithMoreFields() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ], - 'anotherfield' => ['type' => Type::string()] - ] - ]); + interface AnotherInterface { + field(input: String): String + } - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); + type AnotherObject implements AnotherInterface { + field(input: String): String + anotherField: String + } + '); + + $this->assertEquals( + [], + $schema->validate() + ); } /** @@ -2068,75 +2144,24 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalOptionalArguments() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - 'anotherInput' => ['type' => Type::string()], - ] - ] - ] - ]); + interface AnotherInterface { + field(input: String): String + } - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); - } + type AnotherObject implements AnotherInterface { + field(input: String, anotherInput: String): String + } + '); - /** - * @it rejects an Object which implements an Interface field along with additional required arguments - */ - public function testRejectsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalRequiredArguments() - { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ] - ] - ]); - - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - 'anotherInput' => ['type' => Type::nonNull(Type::string())], - ] - ] - ] - ]); - - $schema = $this->schemaWithFieldType($AnotherObject); - - $this->setExpectedException( - InvariantViolation::class, - 'AnotherObject.field(anotherInput:) is of required type "String!" but is not also provided by the interface AnotherInterface.field.' + $this->assertEquals( + [], + $schema->validate() ); - - $schema->assertValid(); } /** @@ -2144,33 +2169,26 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectMissingAnInterfaceField() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'anotherfield' => ['type' => Type::string()] - ] - ]); + interface AnotherInterface { + field(input: String): String + } - $schema = $this->schemaWithFieldType($AnotherObject); + type AnotherObject implements AnotherInterface { + anotherField: String + } + '); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface expects field "field" but AnotherObject does not provide it' + $this->assertContainsValidationMessage( + $schema->validate(), + '"AnotherInterface" expects field "field" but ' . + '"AnotherObject" does not provide it.', + [['line' => 7, 'column' => 9], ['line' => 10, 'column' => 7]] ); - $schema->assertValid(); } /** @@ -2178,27 +2196,26 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectWithAnIncorrectlyTypedInterfaceField() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => Type::string()] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => $this->SomeScalarType] - ] - ]); + interface AnotherInterface { + field(input: String): String + } - $schema = $this->schemaWithFieldType($AnotherObject); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field expects type "String" but AnotherObject.field provides type "SomeScalar"' + type AnotherObject implements AnotherInterface { + field(input: String): Int + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects type "String" but ' . + 'AnotherObject.field is type "Int".', + [['line' => 7, 'column' => 31], ['line' => 11, 'column' => 31]] ); - $schema->assertValid(); } /** @@ -2206,43 +2223,29 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectWithADifferentlyTypedInterfaceField() { - $TypeA = new ObjectType([ - 'name' => 'A', - 'fields' => [ - 'foo' => ['type' => Type::string()] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $TypeB = new ObjectType([ - 'name' => 'B', - 'fields' => [ - 'foo' => ['type' => Type::string()] - ] - ]); + type A { foo: String } + type B { foo: String } - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => $TypeA] - ] - ]); + interface AnotherInterface { + field: A + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => $TypeB] - ] - ]); + type AnotherObject implements AnotherInterface { + field: B + } + '); - $schema = $this->schemaWithFieldType($AnotherObject); - - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field expects type "A" but AnotherObject.field provides type "B"' + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects type "A" but ' . + 'AnotherObject.field is type "B".', + [['line' => 10, 'column' => 16], ['line' => 14, 'column' => 16]] ); - - $schema->assertValid(); } /** @@ -2250,27 +2253,24 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWithASubtypedInterfaceFieldForInterface() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => function () use (&$AnotherInterface) { - return [ - 'field' => ['type' => $AnotherInterface] - ]; - } - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => function () use (&$AnotherObject) { - return [ - 'field' => ['type' => $AnotherObject] - ]; - } - ]); + interface AnotherInterface { + field: AnotherInterface + } - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); + type AnotherObject implements AnotherInterface { + field: AnotherObject + } + '); + + $this->assertEquals( + [], + $schema->validate() + ); } /** @@ -2278,23 +2278,30 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWithASubtypedInterfaceFieldForUnion() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => $this->SomeUnionType] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => $this->SomeObjectType] - ] - ]); + type SomeObject { + field: String + } - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); + union SomeUnionType = SomeObject + + interface AnotherInterface { + field: SomeUnionType + } + + type AnotherObject implements AnotherInterface { + field: SomeObject + } + '); + + $this->assertEquals( + [], + $schema->validate() + ); } /** @@ -2302,36 +2309,26 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectMissingAnInterfaceArgument() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - ] - ] - ]); + interface AnotherInterface { + field(input: String): String + } - $schema = $this->schemaWithFieldType($AnotherObject); + type AnotherObject implements AnotherInterface { + field: String + } + '); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field expects argument "input" but AnotherObject.field does not provide it.' + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects argument "input" but ' . + 'AnotherObject.field does not provide it.', + [['line' => 7, 'column' => 15], ['line' => 11, 'column' => 9]] ); - - $schema->assertValid(); } /** @@ -2339,39 +2336,87 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectWithAnIncorrectlyTypedInterfaceArgument() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => Type::string()], - ] - ] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => [ - 'type' => Type::string(), - 'args' => [ - 'input' => ['type' => $this->SomeScalarType], - ] - ] - ] - ]); + interface AnotherInterface { + field(input: String): String + } - $schema = $this->schemaWithFieldType($AnotherObject); + type AnotherObject implements AnotherInterface { + field(input: Int): String + } + '); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field(input:) expects type "String" but AnotherObject.field(input:) provides type "SomeScalar".' + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field(input:) expects type "String" but ' . + 'AnotherObject.field(input:) is type "Int".', + [['line' => 7, 'column' => 22], ['line' => 11, 'column' => 22]] ); + } - $schema->assertValid(); + /** + * @it rejects an Object with both an incorrectly typed field and argument + */ + public function testRejectsAnObjectWithBothAnIncorrectlyTypedFieldAndArgument() + { + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } + + interface AnotherInterface { + field(input: String): String + } + + type AnotherObject implements AnotherInterface { + field(input: Int): Int + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects type "String" but ' . + 'AnotherObject.field is type "Int".', + [['line' => 7, 'column' => 31], ['line' => 11, 'column' => 28]] + ); + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field(input:) expects type "String" but ' . + 'AnotherObject.field(input:) is type "Int".', + [['line' => 7, 'column' => 22], ['line' => 11, 'column' => 22]] + ); + } + + /** + * @it rejects an Object which implements an Interface field along with additional required arguments + */ + public function testRejectsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalRequiredArguments() + { + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } + + interface AnotherInterface { + field(input: String): String + } + + type AnotherObject implements AnotherInterface { + field(input: String, anotherInput: String!): String + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherObject.field(anotherInput:) is of required type ' . + '"String!" but is not also provided by the interface ' . + 'AnotherInterface.field.', + [['line' => 11, 'column' => 44], ['line' => 7, 'column' => 9]] + ); } /** @@ -2379,23 +2424,24 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWithAnEquivalentlyModifiedInterfaceFieldType() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => Type::nonNull(Type::listOf(Type::string()))] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => Type::nonNull(Type::listOf(Type::string()))] - ] - ]); + interface AnotherInterface { + field: [String]! + } - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); + type AnotherObject implements AnotherInterface { + field: [String]! + } + '); + + $this->assertEquals( + [], + $schema->validate() + ); } /** @@ -2403,29 +2449,26 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectWithANonListInterfaceFieldListType() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => Type::listOf(Type::string())] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => Type::string()] - ] - ]); + interface AnotherInterface { + field: [String] + } - $schema = $this->schemaWithFieldType($AnotherObject); + type AnotherObject implements AnotherInterface { + field: String + } + '); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field expects type "[String]" but AnotherObject.field provides type "String"' + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects type "[String]" but ' . + 'AnotherObject.field is type "String".', + [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]] ); - - $schema->assertValid(); } /** @@ -2433,27 +2476,26 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectWithAListInterfaceFieldNonListType() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => Type::string()] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => Type::listOf(Type::string())] - ] - ]); + interface AnotherInterface { + field: String + } - $schema = $this->schemaWithFieldType($AnotherObject); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field expects type "String" but AnotherObject.field provides type "[String]"' + type AnotherObject implements AnotherInterface { + field: [String] + } + '); + + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects type "String" but ' . + 'AnotherObject.field is type "[String]".', + [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]] ); - $schema->assertValid(); } /** @@ -2461,23 +2503,24 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testAcceptsAnObjectWithASubsetNonNullInterfaceFieldType() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => Type::string()] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => Type::nonNull(Type::string())] - ] - ]); + interface AnotherInterface { + field: String + } - $schema = $this->schemaWithFieldType($AnotherObject); - $schema->assertValid(); + type AnotherObject implements AnotherInterface { + field: String! + } + '); + + $this->assertEquals( + [], + $schema->validate() + ); } /** @@ -2485,29 +2528,26 @@ class ValidationTest extends \PHPUnit_Framework_TestCase */ public function testRejectsAnObjectWithASupersetNullableInterfaceFieldType() { - $AnotherInterface = new InterfaceType([ - 'name' => 'AnotherInterface', - 'fields' => [ - 'field' => ['type' => Type::nonNull(Type::string())] - ] - ]); + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } - $AnotherObject = new ObjectType([ - 'name' => 'AnotherObject', - 'interfaces' => [$AnotherInterface], - 'fields' => [ - 'field' => ['type' => Type::string()] - ] - ]); + interface AnotherInterface { + field: String! + } - $schema = $this->schemaWithFieldType($AnotherObject); + type AnotherObject implements AnotherInterface { + field: String + } + '); - $this->setExpectedException( - InvariantViolation::class, - 'AnotherInterface.field expects type "String!" but AnotherObject.field provides type "String"' + $this->assertContainsValidationMessage( + $schema->validate(), + 'AnotherInterface.field expects type "String!" but ' . + 'AnotherObject.field is type "String".', + [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]] ); - - $schema->assertValid(); } /** @@ -2665,25 +2705,6 @@ class ValidationTest extends \PHPUnit_Framework_TestCase ]); } - private function schemaWithObjectImplementingType($implementedType) - { - $BadObjectType = new ObjectType([ - 'name' => 'BadObject', - 'interfaces' => [$implementedType], - 'fields' => ['f' => ['type' => Type::string()]] - ]); - - return new Schema([ - 'query' => new ObjectType([ - 'name' => 'Query', - 'fields' => [ - 'f' => ['type' => $BadObjectType] - ] - ]), - 'types' => [$BadObjectType] - ]); - } - private function withModifiers($types) { return array_merge( diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index 6c4eaa6..cfd21f7 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -863,21 +863,6 @@ type Query { // 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 */ @@ -893,25 +878,6 @@ 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 }