Deleted unused SchemaValidator

This commit is contained in:
vladar 2016-12-08 06:05:40 +07:00
parent 90e29ac704
commit ae57a72461
2 changed files with 0 additions and 864 deletions

View File

@ -1,271 +0,0 @@
<?php
namespace GraphQL\Type;
use GraphQL\Error\Error;
use GraphQL\Schema;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils;
class SchemaValidator
{
private static $rules;
public static function getAllRules()
{
if (null === self::$rules) {
self::$rules = [
self::noInputTypesAsOutputFieldsRule(),
self::noOutputTypesAsInputArgsRule(),
self::typesInterfacesMustShowThemAsPossibleRule(),
self::interfacePossibleTypesMustImplementTheInterfaceRule(),
self::interfacesAreCorrectlyImplemented()
];
}
return self::$rules;
}
public static function noInputTypesAsOutputFieldsRule()
{
return function ($context) {
$operationMayNotBeInputType = function (Type $type, $operation) {
if (!Type::isOutputType($type)) {
return new Error("Schema $operation must be Object Type but got: $type.");
}
return null;
};
/** @var Schema $schema */
$schema = $context['schema'];
$typeMap = $schema->getTypeMap();
$errors = [];
$queryType = $schema->getQueryType();
if ($queryType) {
$queryError = $operationMayNotBeInputType($queryType, 'query');
if ($queryError !== null) {
$errors[] = $queryError;
}
}
$mutationType = $schema->getMutationType();
if ($mutationType) {
$mutationError = $operationMayNotBeInputType($mutationType, 'mutation');
if ($mutationError !== null) {
$errors[] = $mutationError;
}
}
$subscriptionType = $schema->getSubscriptionType();
if ($subscriptionType) {
$subscriptionError = $operationMayNotBeInputType($subscriptionType, 'subscription');
if ($subscriptionError !== null) {
$errors[] = $subscriptionError;
}
}
foreach ($typeMap as $typeName => $type) {
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
$fields = $type->getFields();
foreach ($fields as $fieldName => $field) {
if ($field->getType() instanceof InputObjectType) {
$errors[] = new Error(
"Field $typeName.{$field->name} is of type " .
"{$field->getType()->name}, which is an input type, but field types " .
"must be output types!"
);
}
}
}
}
return !empty($errors) ? $errors : null;
};
}
public static function noOutputTypesAsInputArgsRule()
{
return function($context) {
/** @var Schema $schema */
$schema = $context['schema'];
$typeMap = $schema->getTypeMap();
$errors = [];
foreach ($typeMap as $typeName => $type) {
if ($type instanceof InputObjectType) {
$fields = $type->getFields();
foreach ($fields as $fieldName => $field) {
if (!Type::isInputType($field->getType())) {
$errors[] = new Error(
"Input field {$type->name}.{$field->name} has type ".
"{$field->getType()}, which is not an input type!"
);
}
}
}
}
return !empty($errors) ? $errors : null;
};
}
public static function interfacePossibleTypesMustImplementTheInterfaceRule()
{
return function($context) {
/** @var Schema $schema */
$schema = $context['schema'];
$typeMap = $schema->getTypeMap();
$errors = [];
foreach ($typeMap as $typeName => $type) {
if ($type instanceof InterfaceType) {
$possibleTypes = $type->getPossibleTypes();
foreach ($possibleTypes as $possibleType) {
if (!in_array($type, $possibleType->getInterfaces())) {
$errors[] = new Error(
"$possibleType is a possible type of interface $type but does " .
"not implement it!"
);
}
}
}
}
return !empty($errors) ? $errors : null;
};
}
public static function typesInterfacesMustShowThemAsPossibleRule()
{
return function($context) {
/** @var Schema $schema */
$schema = $context['schema'];
$typeMap = $schema->getTypeMap();
$errors = [];
foreach ($typeMap as $typeName => $type) {
if ($type instanceof ObjectType) {
$interfaces = $type->getInterfaces();
foreach ($interfaces as $interfaceType) {
if (!$interfaceType->isPossibleType($type)) {
$errors[] = new Error(
"$typeName implements interface {$interfaceType->name}, but " .
"{$interfaceType->name} does not list it as possible!"
);
}
}
}
}
return !empty($errors) ? $errors : null;
};
}
// Enforce correct interface implementations
public static function interfacesAreCorrectlyImplemented()
{
return function($context) {
/** @var Schema $schema */
$schema = $context['schema'];
$errors = [];
foreach ($schema->getTypeMap() as $typeName => $type) {
if ($type instanceof ObjectType) {
foreach ($type->getInterfaces() as $iface) {
try {
// FIXME: rework to return errors instead
self::assertObjectImplementsInterface($schema, $type, $iface);
} catch (\Exception $e) {
$errors[] = $e;
}
}
}
}
return $errors;
};
}
/**
* @param ObjectType $object
* @param InterfaceType $iface
* @throws \Exception
*/
private static function assertObjectImplementsInterface(Schema $schema, ObjectType $object, InterfaceType $iface)
{
$objectFieldMap = $object->getFields();
$ifaceFieldMap = $iface->getFields();
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
Utils::invariant(
isset($objectFieldMap[$fieldName]),
"\"$iface\" expects field \"$fieldName\" but \"$object\" does not provide it"
);
/** @var $ifaceField FieldDefinition */
/** @var $objectField FieldDefinition */
$objectField = $objectFieldMap[$fieldName];
Utils::invariant(
Utils\TypeInfo::isTypeSubTypeOf($schema, $objectField->getType(), $ifaceField->getType()),
"$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " .
"$object.$fieldName provides type \"{$objectField->getType()}\"."
);
foreach ($ifaceField->args as $ifaceArg) {
/** @var $ifaceArg FieldArgument */
/** @var $objectArg FieldArgument */
$argName = $ifaceArg->name;
$objectArg = $objectField->getArg($argName);
// Assert interface field arg exists on object field.
Utils::invariant(
$objectArg,
"$iface.$fieldName expects argument \"$argName\" but $object.$fieldName does not provide it."
);
// Assert interface field arg type matches object field arg type.
// (invariant)
Utils::invariant(
Utils\TypeInfo::isEqualType($ifaceArg->getType(), $objectArg->getType()),
"$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " .
"but $object.$fieldName($argName:) provides " .
"type \"{$objectArg->getType()}\""
);
// Assert argument set invariance.
foreach ($objectField->args as $objectArg) {
$argName = $objectArg->name;
$ifaceArg = $ifaceField->getArg($argName);
Utils::invariant(
$ifaceArg,
"$iface.$fieldName does not define argument \"$argName\" but " .
"$object.$fieldName provides it."
);
}
}
}
}
/**
* @param Schema $schema
* @param array <callable>|null $argRules
* @return array
*/
public static function validate(Schema $schema, $argRules = null)
{
$context = ['schema' => $schema];
$errors = [];
$rules = $argRules ?: self::getAllRules();
for ($i = 0; $i < count($rules); ++$i) {
$newErrors = call_user_func($rules[$i], $context);
if ($newErrors) {
$errors = array_merge($errors, $newErrors);
}
}
return $errors;
}
}

View File

@ -1,593 +0,0 @@
<?php
namespace GraphQL\Tests\Type;
use GraphQL\Schema;
use GraphQL\Type\Definition\Config;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Type\SchemaValidator;
use GraphQL\Utils;
class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
{
private $someInputObjectType;
private $someScalarType;
private $someObjectType;
private $objectWithIsTypeOf;
private $someEnumType;
private $someUnionType;
private $someInterfaceType;
private $outputTypes;
private $noOutputTypes;
private $inputTypes;
private $noInputTypes;
public function setUp()
{
$this->someScalarType = new CustomScalarType([
'name' => 'SomeScalar',
'serialize' => function() {},
'parseValue' => function() {},
'parseLiteral' => function() {}
]);
$this->someObjectType = new ObjectType([
'name' => 'SomeObject',
'fields' => ['f' => ['type' => Type::string()]]
]);
$this->objectWithIsTypeOf = new ObjectType([
'name' => 'ObjectWithIsTypeOf',
'isTypeOf' => function() {return true;},
'fields' => ['f' => ['type' => Type::string()]]
]);
$this->someUnionType = new UnionType([
'name' => 'SomeUnion',
'resolveType' => function() {return null;},
'types' => [$this->someObjectType]
]);
$this->someInterfaceType = new InterfaceType([
'name' => 'SomeInterface',
'resolveType' => function() {return null;},
'fields' => ['f' => ['type' => Type::string()]]
]);
$this->someEnumType = new EnumType([
'name' => 'SomeEnum',
'resolveType' => function() {return null;},
'fields' => ['f' => ['type' => Type::string()]]
]);
$this->someInputObjectType = new InputObjectType([
'name' => 'SomeInputObject',
'fields' => [
'val' => ['type' => Type::float(), 'defaultValue' => 42]
]
]);
$this->outputTypes = $this->withModifiers([
Type::string(),
$this->someScalarType,
$this->someEnumType,
$this->someObjectType,
$this->someUnionType,
$this->someInterfaceType
]);
$this->noOutputTypes = $this->withModifiers([
$this->someInputObjectType
]);
$this->noOutputTypes[] = 'SomeString';
$this->inputTypes = $this->withModifiers([
Type::string(),
$this->someScalarType,
$this->someEnumType,
$this->someInputObjectType
]);
$this->noInputTypes = $this->withModifiers([
$this->someObjectType,
$this->someUnionType,
$this->someInterfaceType
]);
$this->noInputTypes[] = 'SomeString';
}
private function withModifiers($types)
{
return array_merge(
Utils::map($types, function($type) {return Type::listOf($type);}),
Utils::map($types, function($type) {return Type::nonNull($type);}),
Utils::map($types, function($type) {return Type::nonNull(Type::listOf($type));})
);
}
private function schemaWithFieldType($type)
{
return [
'query' => new ObjectType([
'name' => 'Query',
'fields' => ['f' => ['type' => $type]]
]),
'types' => [$type],
];
}
private function expectPasses($schemaConfig)
{
$schema = new Schema(['validate' => true] + $schemaConfig);
$errors = SchemaValidator::validate($schema);
$this->assertEquals([], $errors);
}
private function expectFails($schemaConfig, $error)
{
try {
$schema = new Schema($schemaConfig);
$errors = SchemaValidator::validate($schema);
if ($errors) {
throw $errors[0];
}
$this->fail('Expected exception not thrown');
} catch (\Exception $e) {
$this->assertEquals($e->getMessage(), $error);
}
}
// Type System: A Schema must have Object root types
/**
* @it accepts a Schema whose query type is an object type
*/
public function testAcceptsSchemaWithQueryTypeOfObjectType()
{
$this->expectPasses([
'query' => $this->someObjectType
]);
}
/**
* @it accepts a Schema whose query and mutation types are object types
*/
public function testAcceptsSchemaWithQueryAndMutationTypesOfObjectType()
{
$MutationType = new ObjectType([
'name' => 'Mutation',
'fields' => ['edit' => ['type' => Type::string()]]
]);
$this->expectPasses([
'query' => $this->someObjectType,
'mutation' => $MutationType
]);
}
/**
* @it accepts a Schema whose query and subscription types are object types
*/
public function testAcceptsSchemaWhoseQueryAndSubscriptionTypesAreObjectTypes()
{
$SubscriptionType = new ObjectType([
'name' => 'Subscription',
'fields' => ['subscribe' => ['type' => Type::string()]]
]);
$this->expectPasses([
'query' => $this->someObjectType,
'subscription' => $SubscriptionType
]);
}
/**
* @it rejects a Schema without a query type
*/
public function testRejectsSchemaWithoutQueryType()
{
$this->expectFails([], 'Schema query must be Object Type but got: NULL');
}
/**
* @it rejects a Schema whose query type is an input type
*/
public function testRejectsSchemaWhoseQueryTypeIsAnInputType()
{
$this->expectFails(
['query' => $this->someInputObjectType],
'Schema query must be Object Type but got: SomeInputObject'
);
}
/**
* @it rejects a Schema whose mutation type is an input type
*/
public function testRejectsSchemaWhoseMutationTypeIsInputType()
{
$this->expectFails(
['query' => $this->someObjectType, 'mutation' => $this->someInputObjectType],
'Schema mutation must be Object Type if provided but got: SomeInputObject'
);
}
/**
* @it rejects a Schema whose subscription type is an input type
*/
public function testRejectsSchemaWhoseSubscriptionTypeIsInputType()
{
$this->expectFails(
[
'query' => $this->someObjectType,
'subscription' => $this->someInputObjectType
],
'Schema subscription must be Object Type if provided but got: SomeInputObject'
);
}
/**
* @it rejects a Schema whose directives are incorrectly typed
*/
public function testRejectsSchemaWhoseDirectivesAreIncorrectlyTyped()
{
$this->expectFails(
[
'query' => $this->someObjectType,
'directives' => [ 'somedirective' ]
],
'Schema directives must be Directive[] if provided but got array'
);
}
// Type System: A Schema must contain uniquely named types
/**
* @it rejects a Schema which redefines a built-in type
*/
public function testRejectsSchemaWhichRedefinesBuiltInType()
{
$FakeString = new CustomScalarType([
'name' => 'String',
'serialize' => function() {return null;},
]);
$QueryType = new ObjectType([
'name' => 'Query',
'fields' => [
'normal' => [ 'type' => Type::string() ],
'fake' => [ 'type' => $FakeString ],
]
]);
$this->expectFails(
[ 'query' => $QueryType ],
'Schema must contain unique named types but contains multiple types named "String".'
);
}
/**
* @it rejects a Schema which defines an object type twice
*/
public function testRejectsSchemaWhichDefinesObjectTypeTwice()
{
$A = new ObjectType([
'name' => 'SameName',
'fields' => ['f' => ['type' => Type::string()]],
]);
$B = new ObjectType([
'name' => 'SameName',
'fields' => ['f' => ['type' => Type::string()]],
]);
$QueryType = new ObjectType([
'name' => 'Query',
'fields' => [
'a' => ['type' => $A],
'b' => ['type' => $B]
]
]);
$this->expectFails(
['query' => $QueryType],
'Schema must contain unique named types but contains multiple types named "SameName".'
);
}
/**
* @it rejects a Schema which have same named objects implementing an interface
*/
public function testRejectsSchemaWhichHaveSameNamedObjectsImplementingInterface()
{
$AnotherInterface = new InterfaceType([
'name' => 'AnotherInterface',
'resolveType' => function () {
return null;
},
'fields' => ['f' => ['type' => Type::string()]],
]);
$FirstBadObject = new ObjectType([
'name' => 'BadObject',
'interfaces' => [$AnotherInterface],
'fields' => ['f' => ['type' => Type::string()]],
]);
$SecondBadObject = new ObjectType([
'name' => 'BadObject',
'interfaces' => [$AnotherInterface],
'fields' => ['f' => ['type' => Type::string()]],
]);
$QueryType = new ObjectType([
'name' => 'Query',
'fields' => [
'iface' => ['type' => $AnotherInterface],
]
]);
$this->expectFails(
[
'query' => $QueryType,
'types' => [$FirstBadObject, $SecondBadObject]
],
'Schema must contain unique named types but contains multiple types named "BadObject".'
);
}
// Type System: Objects must have fields
/**
* @it accepts an Object type with fields object
*/
public function testAcceptsAnObjectTypeWithFieldsObject()
{
$schemaConfig = $this->schemaWithFieldType(new ObjectType([
'name' => 'SomeObject',
'fields' => [
'f' => [ 'type' => Type::string() ]
]
]));
$this->expectPasses($schemaConfig);
}
/**
* @it accepts an Object type with a field function
*/
public function testAcceptsAnObjectTypeWithFieldFunction()
{
$schemaConfig = $this->schemaWithFieldType(new ObjectType([
'name' => 'SomeObject',
'fields' => function() {
return [
'f' => ['type' => Type::string()]
];
}
]));
$this->expectPasses($schemaConfig);
}
// Type System Config
public function testPassesOnTheIntrospectionSchema()
{
$this->expectPasses(['query' => Introspection::_schema()]);
}
public function testRejectsASchemaThatUsesAnInputTypeAsAField()
{
$enabled = Config::isValidationEnabled();
Config::disableValidation();
$kinds = [
'GraphQL\Type\Definition\ObjectType',
];
foreach ($kinds as $kind) {
$someOutputType = new $kind([
'name' => 'SomeOutputType',
'fields' => function() {
return [
'sneaky' => $this->someInputObjectType
];
}
]);
$schema = new Schema(['query' => $someOutputType]);
$validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]);
$this->assertSame(1, count($validationResult));
$this->assertSame(
'Field SomeOutputType.sneaky is of type SomeInputObject, which is an ' .
'input type, but field types must be output types!',
$validationResult[0]->message
);
}
if ($enabled) {
Config::enableValidation();
}
}
public function testAcceptsASchemaThatSimplyHasAnInputTypeAsAFieldArg()
{
$this->expectToAcceptSchemaWithNormalInputArg(SchemaValidator::noInputTypesAsOutputFieldsRule());
}
private function expectToAcceptSchemaWithNormalInputArg($rule)
{
$someOutputType = new ObjectType([
'name' => 'SomeOutputType',
'fields' => [
'fieldWithArg' => [
'args' => ['someArg' => ['type' => $this->someInputObjectType]],
'type' => Type::float()
]
]
]);
$schema = new Schema(['query' => $someOutputType]);
$errors = SchemaValidator::validate($schema, [$rule]);
$this->assertEmpty($errors);
}
private function checkValidationResult($validationErrors, $operationType)
{
$this->assertNotEmpty($validationErrors, "Should not validate");
$this->assertEquals(1, count($validationErrors));
$this->assertEquals(
"Schema $operationType must be Object Type but got: SomeInputObject.",
$validationErrors[0]->message
);
}
// Rule: NoOutputTypesAsInputArgs
public function testAcceptsASchemaThatSimplyHasAnInputTypeAsAFieldArg2()
{
$this->expectToAcceptSchemaWithNormalInputArg(SchemaValidator::noOutputTypesAsInputArgsRule());
}
public function testRejectsASchemaWithAnObjectTypeAsAnInputFieldArg()
{
// rejects a schema with an object type as an input field arg
$someOutputType = new ObjectType([
'name' => 'SomeOutputType',
'fields' => ['f' => ['type' => Type::float()]]
]);
$this->assertRejectingFieldArgOfType($someOutputType);
}
public function testRejectsASchemaWithAUnionTypeAsAnInputFieldArg()
{
// rejects a schema with a union type as an input field arg
$unionType = new UnionType([
'name' => 'UnionType',
'types' => [
new ObjectType([
'name' => 'SomeOutputType',
'fields' => [ 'f' => [ 'type' => Type::float() ] ]
])
]
]);
$this->assertRejectingFieldArgOfType($unionType);
}
public function testRejectsASchemaWithAnInterfaceTypeAsAnInputFieldArg()
{
// rejects a schema with an interface type as an input field arg
$interfaceType = new InterfaceType([
'name' => 'InterfaceType',
'fields' => []
]);
$this->assertRejectingFieldArgOfType($interfaceType);
}
public function testRejectsASchemaWithAListOfObjectsAsAnInputFieldArg()
{
// rejects a schema with a list of objects as an input field arg
$listObjects = new ListOfType(new ObjectType([
'name' => 'SomeInputObject',
'fields' => ['f' => ['type' => Type::float()]]
]));
$this->assertRejectingFieldArgOfType($listObjects);
}
public function testRejectsASchemaWithANonnullObjectAsAnInputFieldArg()
{
// rejects a schema with a nonnull object as an input field arg
$nonNullObject = new NonNull(new ObjectType([
'name' => 'SomeOutputType',
'fields' => [ 'f' => [ 'type' => Type::float() ] ]
]));
$this->assertRejectingFieldArgOfType($nonNullObject);
}
public function testAcceptsSchemaWithListOfInputTypeAsInputFieldArg()
{
// accepts a schema with a list of input type as an input field arg
$this->assertAcceptingFieldArgOfType(new ListOfType(new InputObjectType([
'name' => 'SomeInputObject'
])));
}
public function testAcceptsSchemaWithNonnullInputTypeAsInputFieldArg()
{
// accepts a schema with a nonnull input type as an input field arg
$this->assertAcceptingFieldArgOfType(new NonNull(new InputObjectType([
'name' => 'SomeInputObject'
])));
}
private function assertRejectingFieldArgOfType($fieldArgType)
{
$schema = $this->schemaWithFieldArgOfType($fieldArgType);
$validationResult = SchemaValidator::validate($schema, [SchemaValidator::noOutputTypesAsInputArgsRule()]);
$this->expectRejectionBecauseFieldIsNotInputType($validationResult, $fieldArgType);
}
private function assertAcceptingFieldArgOfType($fieldArgType)
{
$schema = $this->schemaWithFieldArgOfType($fieldArgType);
$errors = SchemaValidator::validate($schema, [SchemaValidator::noOutputTypesAsInputArgsRule()]);
$this->assertEmpty($errors);
}
private function schemaWithFieldArgOfType($argType)
{
$someIncorrectInputType = new InputObjectType([
'name' => 'SomeIncorrectInputType',
'fields' => function() use ($argType) {
return [
'val' => ['type' => $argType ]
];
}
]);
$queryType = new ObjectType([
'name' => 'QueryType',
'fields' => [
'f2' => [
'type' => Type::float(),
'args' => ['arg' => [ 'type' => $someIncorrectInputType] ]
]
]
]);
return new Schema(['query' => $queryType]);
}
private function expectRejectionBecauseFieldIsNotInputType($errors, $fieldTypeName)
{
$this->assertSame(1, count($errors));
$this->assertSame(
"Input field SomeIncorrectInputType.val has type $fieldTypeName, " .
"which is not an input type!",
$errors[0]->message
);
}
public function testRejectsWhenAPossibleTypeDoesNotImplementTheInterface()
{
// TODO: Validation for interfaces / implementors
}
}