Consistent validation of type names + reject names starting with __

This commit is contained in:
Vladimir Razuvaev 2017-07-03 18:04:08 +07:00
parent b147b528e2
commit b471938f16
9 changed files with 314 additions and 3 deletions

View File

@ -31,6 +31,8 @@ class EnumType extends Type implements InputType, OutputType, LeafType
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name'], !empty($config['isIntrospection']));
Config::validate($config, [ Config::validate($config, [
'name' => Config::NAME | Config::REQUIRED, 'name' => Config::NAME | Config::REQUIRED,
'values' => Config::arrayOf([ 'values' => Config::arrayOf([

View File

@ -29,7 +29,7 @@ class InputObjectType extends Type implements InputType
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::invariant(!empty($config['name']), 'Every type is expected to have name'); Utils::assertValidName($config['name']);
Config::validate($config, [ Config::validate($config, [
'name' => Config::NAME | Config::REQUIRED, 'name' => Config::NAME | Config::REQUIRED,

View File

@ -34,6 +34,8 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name']);
Config::validate($config, [ Config::validate($config, [
'name' => Config::NAME, 'name' => Config::NAME,
'fields' => Config::arrayOf( 'fields' => Config::arrayOf(

View File

@ -79,7 +79,7 @@ class ObjectType extends Type implements OutputType, CompositeType
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::invariant(!empty($config['name']), 'Every type is expected to have name'); Utils::assertValidName($config['name'], !empty($config['isIntrospection']));
// 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

View File

@ -32,6 +32,6 @@ abstract class ScalarType extends Type implements OutputType, InputType, LeafTyp
$this->name = $this->tryInferName(); $this->name = $this->tryInferName();
} }
Utils::invariant($this->name, 'Type must be named.'); Utils::assertValidName($this->name);
} }
} }

View File

@ -34,6 +34,8 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name']);
Config::validate($config, [ Config::validate($config, [
'name' => Config::NAME | Config::REQUIRED, 'name' => Config::NAME | Config::REQUIRED,
'types' => Config::arrayOf(Config::OBJECT_TYPE, Config::MAYBE_THUNK | Config::REQUIRED), 'types' => Config::arrayOf(Config::OBJECT_TYPE, Config::MAYBE_THUNK | Config::REQUIRED),

View File

@ -232,6 +232,7 @@ EOD;
if (!isset(self::$map['__Schema'])) { if (!isset(self::$map['__Schema'])) {
self::$map['__Schema'] = new ObjectType([ self::$map['__Schema'] = new ObjectType([
'name' => '__Schema', 'name' => '__Schema',
'isIntrospection' => true,
'description' => 'description' =>
'A GraphQL Schema defines the capabilities of a GraphQL ' . 'A GraphQL Schema defines the capabilities of a GraphQL ' .
'server. It exposes all available types and directives on ' . 'server. It exposes all available types and directives on ' .
@ -286,6 +287,7 @@ EOD;
if (!isset(self::$map['__Directive'])) { if (!isset(self::$map['__Directive'])) {
self::$map['__Directive'] = new ObjectType([ self::$map['__Directive'] = new ObjectType([
'name' => '__Directive', 'name' => '__Directive',
'isIntrospection' => true,
'description' => 'A Directive provides a way to describe alternate runtime execution and ' . 'description' => 'A Directive provides a way to describe alternate runtime execution and ' .
'type validation behavior in a GraphQL document.' . 'type validation behavior in a GraphQL document.' .
"\n\nIn some cases, you need to provide options to alter GraphQL's " . "\n\nIn some cases, you need to provide options to alter GraphQL's " .
@ -345,6 +347,7 @@ EOD;
if (!isset(self::$map['__DirectiveLocation'])) { if (!isset(self::$map['__DirectiveLocation'])) {
self::$map['__DirectiveLocation'] = new EnumType([ self::$map['__DirectiveLocation'] = new EnumType([
'name' => '__DirectiveLocation', 'name' => '__DirectiveLocation',
'isIntrospection' => true,
'description' => 'description' =>
'A Directive can be adjacent to many parts of the GraphQL language, a ' . 'A Directive can be adjacent to many parts of the GraphQL language, a ' .
'__DirectiveLocation describes one such possible adjacencies.', '__DirectiveLocation describes one such possible adjacencies.',
@ -433,6 +436,7 @@ EOD;
if (!isset(self::$map['__Type'])) { if (!isset(self::$map['__Type'])) {
self::$map['__Type'] = new ObjectType([ self::$map['__Type'] = new ObjectType([
'name' => '__Type', 'name' => '__Type',
'isIntrospection' => true,
'description' => 'description' =>
'The fundamental unit of any GraphQL Schema is the type. There are ' . 'The fundamental unit of any GraphQL Schema is the type. There are ' .
'many kinds of types in GraphQL as represented by the `__TypeKind` enum.' . 'many kinds of types in GraphQL as represented by the `__TypeKind` enum.' .
@ -560,6 +564,7 @@ EOD;
self::$map['__Field'] = new ObjectType([ self::$map['__Field'] = new ObjectType([
'name' => '__Field', 'name' => '__Field',
'isIntrospection' => true,
'description' => 'description' =>
'Object and Interface types are described by a list of Fields, each of ' . 'Object and Interface types are described by a list of Fields, each of ' .
'which has a name, potentially a list of arguments, and a return type.', 'which has a name, potentially a list of arguments, and a return type.',
@ -600,6 +605,7 @@ EOD;
if (!isset(self::$map['__InputValue'])) { if (!isset(self::$map['__InputValue'])) {
self::$map['__InputValue'] = new ObjectType([ self::$map['__InputValue'] = new ObjectType([
'name' => '__InputValue', 'name' => '__InputValue',
'isIntrospection' => true,
'description' => 'description' =>
'Arguments provided to Fields or Directives and the input fields of an ' . 'Arguments provided to Fields or Directives and the input fields of an ' .
'InputObject are represented as Input Values which describe their type ' . 'InputObject are represented as Input Values which describe their type ' .
@ -637,6 +643,7 @@ EOD;
if (!isset(self::$map['__EnumValue'])) { if (!isset(self::$map['__EnumValue'])) {
self::$map['__EnumValue'] = new ObjectType([ self::$map['__EnumValue'] = new ObjectType([
'name' => '__EnumValue', 'name' => '__EnumValue',
'isIntrospection' => true,
'description' => 'description' =>
'One possible value for a given Enum. Enum values are unique values, not ' . 'One possible value for a given Enum. Enum values are unique values, not ' .
'a placeholder for a string or numeric value. However an Enum value is ' . 'a placeholder for a string or numeric value. However an Enum value is ' .
@ -664,6 +671,7 @@ EOD;
if (!isset(self::$map['__TypeKind'])) { if (!isset(self::$map['__TypeKind'])) {
self::$map['__TypeKind'] = new EnumType([ self::$map['__TypeKind'] = new EnumType([
'name' => '__TypeKind', 'name' => '__TypeKind',
'isIntrospection' => true,
'description' => 'An enum describing what kind of type a given `__Type` is.', 'description' => 'An enum describing what kind of type a given `__Type` is.',
'values' => [ 'values' => [
'SCALAR' => [ 'SCALAR' => [

View File

@ -331,4 +331,33 @@ class Utils
// Otherwise print the escaped form. // Otherwise print the escaped form.
: '"\\u' . dechex($code) . '"'; : '"\\u' . dechex($code) . '"';
} }
/**
* @param $name
* @param bool $isIntrospection
* @throws Error
*/
public static function assertValidName($name, $isIntrospection = false)
{
$regex = '/^[_a-zA-Z][_a-zA-Z0-9]*$/';
if (!$name || !is_string($name)) {
throw new InvariantViolation(
"Must be named. Unexpected name: " . self::printSafe($name)
);
}
if (!$isIntrospection && isset($name[1]) && $name[0] === '_' && $name[1] === '_') {
throw new InvariantViolation(
'Name "'.$name.'" must not begin with "__", which is reserved by ' .
'GraphQL introspection.'
);
}
if (!preg_match($regex, $name)) {
throw new InvariantViolation(
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "'.$name.'" does not.'
);
}
}
} }

View File

@ -0,0 +1,268 @@
<?php
namespace GraphQL\Tests\Type;
use GraphQL\Error\InvariantViolation;
use GraphQL\Schema;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\UnionType;
class ValidationTest extends \PHPUnit_Framework_TestCase
{
public function testRejectsTypesWithoutNames()
{
$this->assertEachCallableThrows([
function() {
return new ObjectType([]);
},
function() {
return new EnumType([]);
},
function() {
return new InputObjectType([]);
},
function() {
return new UnionType([]);
},
function() {
return new InterfaceType([]);
}
], 'Must be named. Unexpected name: null');
}
public function testRejectsAnObjectTypeWithReservedName()
{
$this->assertEachCallableThrows([
function() {
return new ObjectType([
'name' => '__ReservedName',
]);
},
function() {
return new EnumType([
'name' => '__ReservedName',
]);
},
function() {
return new InputObjectType([
'name' => '__ReservedName',
]);
},
function() {
return new UnionType([
'name' => '__ReservedName',
]);
},
function() {
return new InterfaceType([
'name' => '__ReservedName',
]);
}
], 'Name "__ReservedName" must not begin with "__", which is reserved by GraphQL introspection.');
}
public function testRejectsAnObjectTypeWithInvalidName()
{
$this->assertEachCallableThrows([
function() {
return new ObjectType([
'name' => 'a-b-c',
]);
},
function() {
return new EnumType([
'name' => 'a-b-c',
]);
},
function() {
return new InputObjectType([
'name' => 'a-b-c',
]);
},
function() {
return new UnionType([
'name' => 'a-b-c',
]);
},
function() {
return new InterfaceType([
'name' => 'a-b-c',
]);
}
], 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "a-b-c" does not.');
}
// DESCRIBE: Type System: A Schema must have Object root types
// TODO: accepts a Schema whose query type is an object type
// TODO: accepts a Schema whose query and mutation types are object types
// TODO: accepts a Schema whose query and subscription types are object types
// TODO: rejects a Schema without a query type
// TODO: rejects a Schema whose query type is an input type
// TODO: rejects a Schema whose mutation type is an input type
// TODO: rejects a Schema whose subscription type is an input type
// TODO: rejects a Schema whose directives are incorrectly typed
// DESCRIBE: Type System: A Schema must contain uniquely named types
// TODO: rejects a Schema which redefines a built-in type
// TODO: rejects a Schema which defines an object type twice
// TODO: rejects a Schema which have same named objects implementing an interface
// DESCRIBE: Type System: Objects must have fields
// TODO: accepts an Object type with fields object
// TODO: accepts an Object type with a field function
// TODO: rejects an Object type with missing fields
// TODO: rejects an Object type with incorrectly named fields
// TODO: rejects an Object type with reserved named fields
// TODO: rejects an Object type with incorrectly typed fields
// TODO: rejects an Object type with empty fields
// TODO: rejects an Object type with a field function that returns nothing
// TODO: rejects an Object type with a field function that returns empty
// DESCRIBE: Type System: Fields args must be properly named
// TODO: accepts field args with valid names
// TODO: rejects field arg with invalid names
// DESCRIBE: Type System: Fields args must be objects
// TODO: accepts an Object type with field args
// TODO: rejects an Object type with incorrectly typed field args
// DESCRIBE: Type System: Object interfaces must be array
// TODO: accepts an Object type with array interfaces
// TODO: accepts an Object type with interfaces as a function returning an array
// TODO: rejects an Object type with incorrectly typed interfaces
// TODO: rejects an Object type with interfaces as a function returning an incorrect type
// DESCRIBE: Type System: Union types must be array
// TODO: accepts a Union type with array types
// TODO: accepts a Union type with function returning an array of types
// TODO: rejects a Union type without types
// TODO: rejects a Union type with empty types
// TODO: rejects a Union type with incorrectly typed types
// DESCRIBE: Type System: Input Objects must have fields
// TODO: accepts an Input Object type with fields
// TODO: accepts an Input Object type with a field function
// TODO: rejects an Input Object type with missing fields
// TODO: rejects an Input Object type with incorrectly typed fields
// TODO: rejects an Input Object type with empty fields
// TODO: rejects an Input Object type with a field function that returns nothing
// TODO: rejects an Input Object type with a field function that returns empty
// DESCRIBE: Type System: Object types must be assertable
// TODO: accepts an Object type with an isTypeOf function
// TODO: rejects an Object type with an incorrect type for isTypeOf
// DESCRIBE: Type System: Interface types must be resolvable
// TODO: accepts an Interface type defining resolveType
// TODO: accepts an Interface with implementing type defining isTypeOf
// TODO: accepts an Interface type defining resolveType with implementing type defining isTypeOf
// TODO: rejects an Interface type with an incorrect type for resolveType
// TODO: rejects an Interface type not defining resolveType with implementing type not defining isTypeOf
// DESCRIBE: Type System: Union types must be resolvable
// TODO: accepts a Union type defining resolveType
// TODO: accepts a Union of Object types defining isTypeOf
// TODO: accepts a Union type defining resolveType of Object types defining isTypeOf
// TODO: rejects an Interface type with an incorrect type for resolveType
// TODO: rejects a Union type not defining resolveType of Object types not defining isTypeOf
// DESCRIBE: Type System: Scalar types must be serializable
// TODO: accepts a Scalar type defining serialize
// TODO: rejects a Scalar type not defining serialize
// TODO: rejects a Scalar type defining serialize with an incorrect type
// TODO: accepts a Scalar type defining parseValue and parseLiteral
// TODO: rejects a Scalar type defining parseValue but not parseLiteral
// TODO: rejects a Scalar type defining parseLiteral but not parseValue
// TODO: rejects a Scalar type defining parseValue and parseLiteral with an incorrect type
// DESCRIBE: Type System: Enum types must be well defined
// TODO: accepts a well defined Enum type with empty value definition
// TODO: accepts a well defined Enum type with internal value definition
// TODO: rejects an Enum type without values
// TODO: rejects an Enum type with empty values
// TODO: rejects an Enum type with incorrectly typed values
// TODO: rejects an Enum type with missing value definition
// TODO: rejects an Enum type with incorrectly typed value definition
// DESCRIBE: Type System: Object fields must have output types
// TODO: accepts an output type as an Object field type
// TODO: rejects an empty Object field type
// TODO: rejects a non-output type as an Object field type
// DESCRIBE: Type System: Objects can only implement interfaces
// TODO: accepts an Object implementing an Interface
// DESCRIBE: Type System: Unions must represent Object types
// TODO: accepts a Union of an Object Type
// TODO: rejects a Union of a non-Object type
// DESCRIBE: Type System: Interface fields must have output types
// TODO: accepts an output type as an Interface field type
// TODO: rejects an empty Interface field type
// TODO: rejects a non-output type as an Interface field type
// DESCRIBE: Type System: Field arguments must have input types
// TODO: accepts an input type as a field arg type
// TODO: rejects an empty field arg type
// TODO: rejects a non-input type as a field arg type
// DESCRIBE: Type System: Input Object fields must have input types
// TODO: accepts an input type as an input field type
// TODO: rejects an empty input field type
// TODO: rejects a non-input type as an input field type
// DESCRIBE: Type System: List must accept GraphQL types
// TODO: accepts an type as item type of list: ${type}
// TODO: rejects a non-type as item type of list: ${type}
// DESCRIBE: Type System: NonNull must accept GraphQL types
// TODO: accepts an type as nullable type of non-null: ${type}
// TODO: rejects a non-type as nullable type of non-null: ${type}
// DESCRIBE: Objects must adhere to Interface they implement
// TODO: accepts an Object which implements an Interface
// TODO: accepts an Object which implements an Interface along with more fields
// TODO: accepts an Object which implements an Interface field along with additional optional arguments
// TODO: rejects an Object which implements an Interface field along with additional required arguments
// TODO: rejects an Object missing an Interface field
// TODO: rejects an Object with an incorrectly typed Interface field
// TODO: rejects an Object with a differently typed Interface field
// TODO: accepts an Object with a subtyped Interface field (interface)
// TODO: accepts an Object with a subtyped Interface field (union)
// TODO: rejects an Object missing an Interface argument
// TODO: rejects an Object with an incorrectly typed Interface argument
// TODO: accepts an Object with an equivalently modified Interface field type
// TODO: rejects an Object with a non-list Interface field list type
// TODO: rejects an Object with a list Interface field non-list type
// TODO: accepts an Object with a subset non-null Interface field type
// TODO: rejects an Object with a superset nullable Interface field type
// TODO: does not allow isDeprecated without deprecationReason on field
// TODO: does not allow isDeprecated without deprecationReason on enum
private function assertEachCallableThrows($closures, $expectedError)
{
foreach ($closures as $index => $factory) {
try {
$factory();
$this->fail('Expected exception not thrown for entry ' . $index);
} catch (InvariantViolation $e) {
$this->assertEquals($expectedError, $e->getMessage(), 'Error in callable #' . $index);
}
}
}
private function schemaWithFieldType($type)
{
return new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [ 'f' => $type ]
]),
'types' => [ $type ]
]);
}
}