From 34eae0b891c9d903e379dff04587391290a84b9a Mon Sep 17 00:00:00 2001 From: Vladimir Razuvaev Date: Sun, 13 Aug 2017 02:50:34 +0700 Subject: [PATCH] Schema validation + tests (#148) --- docs/type-system/enum-types.md | 13 - src/Error/Warning.php | 17 +- src/Type/Definition/CustomScalarType.php | 33 +- src/Type/Definition/EnumType.php | 87 +- src/Type/Definition/EnumValueDefinition.php | 12 +- src/Type/Definition/FieldArgument.php | 28 + src/Type/Definition/FieldDefinition.php | 93 +- src/Type/Definition/InputObjectField.php | 6 + src/Type/Definition/InputObjectType.php | 45 + src/Type/Definition/InterfaceType.php | 32 +- src/Type/Definition/ListOfType.php | 11 +- src/Type/Definition/NonNull.php | 15 +- src/Type/Definition/ObjectType.php | 92 +- src/Type/Definition/Type.php | 7 + src/Type/Definition/UnionType.php | 59 +- src/Type/Schema.php | 107 + src/Type/SchemaConfig.php | 3 +- src/Utils/TypeComparators.php | 137 + src/Utils/TypeInfo.php | 102 +- src/Utils/Utils.php | 4 +- .../Rules/VariablesInAllowedPosition.php | 3 +- tests/Executor/LazyInterfaceTest.php | 6 +- tests/ServerTest.php | 4 +- tests/Type/DefinitionTest.php | 8 +- tests/Type/IntrospectionTest.php | 1868 ++++++----- tests/Type/ValidationTest.php | 2929 ++++++++++++++++- tests/Utils/ExtractTypesTest.php | 2 +- 27 files changed, 4489 insertions(+), 1234 deletions(-) create mode 100644 src/Utils/TypeComparators.php diff --git a/docs/type-system/enum-types.md b/docs/type-system/enum-types.md index 6dd684a..58179e4 100644 --- a/docs/type-system/enum-types.md +++ b/docs/type-system/enum-types.md @@ -64,19 +64,6 @@ $episodeEnum = new EnumType([ which is equivalent of: ```php -$episodeEnum = new EnumType([ - 'name' => 'Episode', - 'description' => 'One of the films in the Star Wars Trilogy', - 'values' => [ - 'NEWHOPE' => 'NEWHOPE', - 'EMPIRE' => 'EMPIRE', - 'JEDI' => 'JEDI' - ] -]); -``` - -which is in turn equivalent of: -```php $episodeEnum = new EnumType([ 'name' => 'Episode', 'description' => 'One of the films in the Star Wars Trilogy', diff --git a/src/Error/Warning.php b/src/Error/Warning.php index 12650bb..3cd54b4 100644 --- a/src/Error/Warning.php +++ b/src/Error/Warning.php @@ -14,6 +14,13 @@ final class Warning static $warned = []; + static private $warningHandler; + + public static function setWarningHandler(callable $warningHandler = null) + { + self::$warningHandler = $warningHandler; + } + static function suppress($suppress = true) { if (true === $suppress) { @@ -40,7 +47,10 @@ final class Warning static function warnOnce($errorMessage, $warningId) { - if ((self::$enableWarnings & $warningId) > 0 && !isset(self::$warned[$warningId])) { + if (self::$warningHandler) { + $fn = self::$warningHandler; + $fn($errorMessage, $warningId); + } else if ((self::$enableWarnings & $warningId) > 0 && !isset(self::$warned[$warningId])) { self::$warned[$warningId] = true; trigger_error($errorMessage, E_USER_WARNING); } @@ -48,7 +58,10 @@ final class Warning static function warn($errorMessage, $warningId) { - if ((self::$enableWarnings & $warningId) > 0) { + if (self::$warningHandler) { + $fn = self::$warningHandler; + $fn($errorMessage, $warningId); + } else if ((self::$enableWarnings & $warningId) > 0) { trigger_error($errorMessage, E_USER_WARNING); } } diff --git a/src/Type/Definition/CustomScalarType.php b/src/Type/Definition/CustomScalarType.php index d425c88..4740b29 100644 --- a/src/Type/Definition/CustomScalarType.php +++ b/src/Type/Definition/CustomScalarType.php @@ -1,6 +1,8 @@ config['parseValue'], $value); + if (isset($this->config['parseValue'])) { + return call_user_func($this->config['parseValue'], $value); + } else { + return null; + } } /** @@ -47,6 +53,29 @@ class CustomScalarType extends ScalarType */ public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode) { - return call_user_func($this->config['parseLiteral'], $valueNode); + if (isset($this->config['parseLiteral'])) { + return call_user_func($this->config['parseLiteral'], $valueNode); + } else { + return null; + } + } + + public function assertValid() + { + parent::assertValid(); + + Utils::invariant( + isset($this->config['serialize']) && is_callable($this->config['serialize']), + "{$this->name} must provide \"serialize\" function. If this custom Scalar " . + 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' . + 'functions are also provided.' + ); + if (isset($this->config['parseValue']) || isset($this->config['parseLiteral'])) { + Utils::invariant( + isset($this->config['parseValue']) && isset($this->config['parseLiteral']) && + is_callable($this->config['parseValue']) && is_callable($this->config['parseLiteral']), + "{$this->name} must provide both \"parseValue\" and \"parseLiteral\" functions." + ); + } } } diff --git a/src/Type/Definition/EnumType.php b/src/Type/Definition/EnumType.php index a7c1726..a436144 100644 --- a/src/Type/Definition/EnumType.php +++ b/src/Type/Definition/EnumType.php @@ -1,6 +1,7 @@ name = $config['name']; $this->description = isset($config['description']) ? $config['description'] : null; - $this->values = []; - - if (!empty($config['values'])) { - foreach ($config['values'] as $name => $value) { - if (!is_array($value)) { - if (is_string($name)) { - $value = ['name' => $name, 'value' => $value]; - } else if (is_int($name) && is_string($value)) { - $value = ['name' => $value, 'value' => $value]; - } - } - // value will be equal to name only if 'value' is not set in definition - $this->values[] = new EnumValueDefinition($value + ['name' => $name, 'value' => $name]); - } - } + $this->config = $config; } /** @@ -69,6 +61,33 @@ class EnumType extends Type implements InputType, OutputType, LeafType */ public function getValues() { + if ($this->values === null) { + $this->values = []; + $config = $this->config; + + if (isset($config['values'])) { + if (!is_array($config['values'])) { + throw new InvariantViolation("{$this->name} values must be an array"); + } + foreach ($config['values'] as $name => $value) { + if (is_string($name)) { + if (!is_array($value)) { + throw new InvariantViolation( + "{$this->name}.$name must refer to an associative array with a " . + '"value" key representing an internal value but got: ' . Utils::printSafe($value) + ); + } + $value += ['name' => $name, 'value' => $name]; + } else if (is_int($name) && is_string($value)) { + $value = ['name' => $value, 'value' => $value]; + } else { + throw new InvariantViolation("{$this->name} values must be an array with value names as keys."); + } + $this->values[] = new EnumValueDefinition($value); + } + } + } + return $this->values; } @@ -168,4 +187,42 @@ class EnumType extends Type implements InputType, OutputType, LeafType } return $this->nameLookup; } + + /** + * @throws InvariantViolation + */ + public function assertValid() + { + parent::assertValid(); + + Utils::invariant( + isset($this->config['values']), + "{$this->name} values must be an array." + ); + + $values = $this->getValues(); + + Utils::invariant( + !empty($values), + "{$this->name} values must be not empty." + ); + foreach ($values as $value) { + try { + Utils::assertValidName($value->name); + } catch (InvariantViolation $e) { + throw new InvariantViolation( + "{$this->name} has value with invalid name: " . + Utils::printSafe($value->name) . " ({$e->getMessage()})" + ); + } + Utils::invariant( + !in_array($value->name, ['true', 'false', 'null']), + "{$this->name}: \"{$value->name}\" can not be used as an Enum value." + ); + Utils::invariant( + !isset($value->config['isDeprecated']), + "{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"." + ); + } + } } diff --git a/src/Type/Definition/EnumValueDefinition.php b/src/Type/Definition/EnumValueDefinition.php index 3895399..12bbbe1 100644 --- a/src/Type/Definition/EnumValueDefinition.php +++ b/src/Type/Definition/EnumValueDefinition.php @@ -28,9 +28,19 @@ class EnumValueDefinition */ public $description; + /** + * @var array + */ + public $config; + public function __construct(array $config) { - Utils::assign($this, $config); + $this->name = isset($config['name']) ? $config['name'] : null; + $this->value = isset($config['value']) ? $config['value'] : null; + $this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null; + $this->description = isset($config['description']) ? $config['description'] : null; + + $this->config = $config; } /** diff --git a/src/Type/Definition/FieldArgument.php b/src/Type/Definition/FieldArgument.php index f3719b1..4a2199e 100644 --- a/src/Type/Definition/FieldArgument.php +++ b/src/Type/Definition/FieldArgument.php @@ -1,6 +1,9 @@ defaultValueExists; } + + public function assertValid(FieldDefinition $parentField, Type $parentType) + { + try { + Utils::assertValidName($this->name); + } catch (InvariantViolation $e) { + throw new InvariantViolation( + "{$parentType->name}.{$parentField->name}({$this->name}:) {$e->getMessage()}") + ; + } + $type = $this->type; + if ($type instanceof WrappingType) { + $type = $type->getWrappedType(true); + } + Utils::invariant( + $type instanceof InputType, + "{$parentType->name}.{$parentField->name}({$this->name}): argument type must be " . + "Input Type but got: " . Utils::printSafe($this->type) + ); + Utils::invariant( + $this->description === null || is_string($this->description), + "{$parentType->name}.{$parentField->name}({$this->name}): argument description type must be " . + "string but got: " . Utils::printSafe($this->description) + ); + } } diff --git a/src/Type/Definition/FieldDefinition.php b/src/Type/Definition/FieldDefinition.php index 42793ac..04a0498 100644 --- a/src/Type/Definition/FieldDefinition.php +++ b/src/Type/Definition/FieldDefinition.php @@ -89,28 +89,72 @@ class FieldDefinition ]); } - /** - * @param array|Config $fields - * @param string $parentTypeName - * @return array - */ - public static function createMap(array $fields, $parentTypeName = null) + public static function defineFieldMap(Type $type, $fields) { + if (is_callable($fields)) { + $fields = $fields(); + } + if (!is_array($fields)) { + throw new InvariantViolation( + "{$type->name} fields must be an array or a callable which returns such an array." + ); + } $map = []; foreach ($fields as $name => $field) { if (is_array($field)) { if (!isset($field['name']) && is_string($name)) { $field['name'] = $name; } - $fieldDef = self::create($field, $parentTypeName); + if (isset($field['args']) && !is_array($field['args'])) { + throw new InvariantViolation( + "{$type->name}.{$name} args must be an array." + ); + } + $fieldDef = self::create($field); + } else if ($field instanceof FieldDefinition) { + $fieldDef = $field; + } else { + if (is_string($name) && $field) { + $fieldDef = self::create(['name' => $name, 'type' => $field]); + } else { + throw new InvariantViolation( + "{$type->name}.$name field config must be an array, but got: " . Utils::printSafe($field) + ); + } + } + $map[$fieldDef->name] = $fieldDef; + } + return $map; + } + + /** + * @param array|Config $fields + * @param string $parentTypeName + * @deprecated + * @return array + */ + public static function createMap(array $fields, $parentTypeName = null) + { + trigger_error( + __METHOD__ . ' is deprecated, use ' . __CLASS__ . '::defineFieldMap() instead', + E_USER_DEPRECATED + ); + + $map = []; + foreach ($fields as $name => $field) { + if (is_array($field)) { + if (!isset($field['name']) && is_string($name)) { + $field['name'] = $name; + } + $fieldDef = self::create($field); } else if ($field instanceof FieldDefinition) { $fieldDef = $field; } else { if (is_string($name)) { - $fieldDef = self::create(['name' => $name, 'type' => $field], $parentTypeName); + $fieldDef = self::create(['name' => $name, 'type' => $field]); } else { throw new InvariantViolation( - "Unexpected field definition for type $parentTypeName at key $name: " . Utils::printSafe($field) + "Unexpected field definition for type $parentTypeName at field $name: " . Utils::printSafe($field) ); } } @@ -195,6 +239,37 @@ class FieldDefinition return $this->complexityFn; } + /** + * @param Type $parentType + * @throws InvariantViolation + */ + public function assertValid(Type $parentType) + { + try { + Utils::assertValidName($this->name); + } catch (InvariantViolation $e) { + throw new InvariantViolation("{$parentType->name}.{$this->name}: {$e->getMessage()}"); + } + Utils::invariant( + !isset($this->config['isDeprecated']), + "{$parentType->name}.{$this->name} should provide \"deprecationReason\" instead of \"isDeprecated\"." + ); + + $type = $this->type; + if ($type instanceof WrappingType) { + $type = $type->getWrappedType(true); + } + Utils::invariant( + $type instanceof OutputType, + "{$parentType->name}.{$this->name} field type must be Output Type but got: " . Utils::printSafe($this->type) + ); + Utils::invariant( + $this->resolveFn === null || is_callable($this->resolveFn), + "{$parentType->name}.{$this->name} field resolver must be a function if provided, but got: %s", + Utils::printSafe($this->resolveFn) + ); + } + /** * @param $childrenComplexity * @return mixed diff --git a/src/Type/Definition/InputObjectField.php b/src/Type/Definition/InputObjectField.php index a60f951..f782196 100644 --- a/src/Type/Definition/InputObjectField.php +++ b/src/Type/Definition/InputObjectField.php @@ -27,6 +27,11 @@ class InputObjectField */ public $type; + /** + * @var array + */ + public $config; + /** * Helps to differentiate when `defaultValue` is `null` and when it was not even set initially * @@ -52,6 +57,7 @@ class InputObjectField $this->{$k} = $v; } } + $this->config = $opts; } /** diff --git a/src/Type/Definition/InputObjectType.php b/src/Type/Definition/InputObjectType.php index 01091e7..f6166dd 100644 --- a/src/Type/Definition/InputObjectType.php +++ b/src/Type/Definition/InputObjectType.php @@ -1,6 +1,7 @@ fields = []; $fields = isset($this->config['fields']) ? $this->config['fields'] : []; $fields = is_callable($fields) ? call_user_func($fields) : $fields; + + if (!is_array($fields)) { + throw new InvariantViolation( + "{$this->name} fields must be an array or a callable which returns such an array." + ); + } + foreach ($fields as $name => $field) { if ($field instanceof Type) { $field = ['type' => $field]; @@ -81,4 +89,41 @@ class InputObjectType extends Type implements InputType Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name); return $this->fields[$name]; } + + /** + * @throws InvariantViolation + */ + public function assertValid() + { + parent::assertValid(); + + $fields = $this->getFields(); + + Utils::invariant( + !empty($fields), + "{$this->name} fields must not be empty" + ); + + foreach ($fields as $field) { + try { + Utils::assertValidName($field->name); + } catch (InvariantViolation $e) { + throw new InvariantViolation("{$this->name}.{$field->name}: {$e->getMessage()}"); + } + + $fieldType = $field->type; + if ($fieldType instanceof WrappingType) { + $fieldType = $fieldType->getWrappedType(true); + } + Utils::invariant( + $fieldType instanceof InputType, + "{$this->name}.{$field->name} field type must be Input Type but got: %s.", + Utils::printSafe($field->type) + ); + Utils::invariant( + !isset($field->config['resolve']), + "{$this->name}.{$field->name} field type has a resolve property, but Input Types cannot define resolvers." + ); + } + } } diff --git a/src/Type/Definition/InterfaceType.php b/src/Type/Definition/InterfaceType.php index 3fc903d..1183f32 100644 --- a/src/Type/Definition/InterfaceType.php +++ b/src/Type/Definition/InterfaceType.php @@ -1,6 +1,7 @@ fields) { - $this->fields = []; $fields = isset($this->config['fields']) ? $this->config['fields'] : []; - $fields = is_callable($fields) ? call_user_func($fields) : $fields; - $this->fields = FieldDefinition::createMap($fields, $this->name); + $this->fields = FieldDefinition::defineFieldMap($this, $fields); } return $this->fields; } @@ -95,4 +94,31 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT } return null; } + + /** + * @throws InvariantViolation + */ + public function assertValid() + { + parent::assertValid(); + + $fields = $this->getFields(); + + Utils::invariant( + !isset($this->config['resolveType']) || is_callable($this->config['resolveType']), + "{$this->name} must provide \"resolveType\" as a function." + ); + + Utils::invariant( + !empty($fields), + "{$this->name} fields must not be empty" + ); + + foreach ($fields as $field) { + $field->assertValid($this); + foreach ($field->args as $arg) { + $arg->assertValid($field, $this); + } + } + } } diff --git a/src/Type/Definition/ListOfType.php b/src/Type/Definition/ListOfType.php index 4ddfa2c..95fd9d9 100644 --- a/src/Type/Definition/ListOfType.php +++ b/src/Type/Definition/ListOfType.php @@ -1,6 +1,7 @@ ofType = $type; } diff --git a/src/Type/Definition/NonNull.php b/src/Type/Definition/NonNull.php index 4b3f082..efef27d 100644 --- a/src/Type/Definition/NonNull.php +++ b/src/Type/Definition/NonNull.php @@ -1,6 +1,7 @@ fields) { $fields = isset($this->config['fields']) ? $this->config['fields'] : []; - $fields = is_callable($fields) ? call_user_func($fields) : $fields; - $this->fields = FieldDefinition::createMap($fields, $this->name); + $this->fields = FieldDefinition::defineFieldMap($this, $fields); } return $this->fields; } @@ -132,9 +132,7 @@ class ObjectType extends Type implements OutputType, CompositeType if (null === $this->fields) { $this->getFields(); } - if (!isset($this->fields[$name])) { - throw new Error(sprintf("Field '%s' is not defined for type '%s'", $name, $this->name)); - } + Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name); return $this->fields[$name]; } @@ -147,11 +145,21 @@ 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)) { + throw new InvariantViolation( + "{$this->name} interfaces must be an Array or a callable which returns an Array." + ); + } + $this->interfaces = []; foreach ($interfaces as $iface) { $iface = Type::resolve($iface); if (!$iface instanceof InterfaceType) { - throw new InvariantViolation(sprintf('Expecting interface type, got %s', Utils::printSafe($iface))); + throw new InvariantViolation(sprintf( + '%s may only implement Interface types, it cannot implement %s', + $this->name, + Utils::printSafe($iface) + )); } // TODO: return interfaceMap vs interfaces. Possibly breaking change? $this->interfaces[] = $iface; @@ -161,6 +169,17 @@ class ObjectType extends Type implements OutputType, CompositeType return $this->interfaces; } + private function getInterfaceMap() + { + if (!$this->interfaceMap) { + $this->interfaceMap = []; + foreach ($this->getInterfaces() as $interface) { + $this->interfaceMap[$interface->name] = $interface; + } + } + return $this->interfaceMap; + } + /** * @param InterfaceType $iface * @return bool @@ -182,4 +201,61 @@ class ObjectType extends Type implements OutputType, CompositeType { return isset($this->config['isTypeOf']) ? call_user_func($this->config['isTypeOf'], $value, $context, $info) : null; } + + /** + * Validates type config and throws if one of type options is invalid. + * Note: this method is shallow, it won't validate object fields and their arguments. + * + * @throws InvariantViolation + */ + public function assertValid() + { + parent::assertValid(); + + Utils::invariant( + null === $this->description || is_string($this->description), + "{$this->name} description must be string if set, but it is: " . Utils::printSafe($this->description) + ); + + Utils::invariant( + !isset($this->config['isTypeOf']) || is_callable($this->config['isTypeOf']), + "{$this->name} must provide 'isTypeOf' as a function" + ); + + // getFields() and getInterfaceMap() will do structural validation + $fields = $this->getFields(); + Utils::invariant( + !empty($fields), + "{$this->name} fields must not be empty" + ); + foreach ($fields as $field) { + $field->assertValid($this); + foreach ($field->args as $arg) { + $arg->assertValid($field, $this); + } + } + + $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; + if (!isset($iface->config['resolveType'])) { + Utils::invariant( + isset($this->config['isTypeOf']), + "Interface Type {$iface->name} does not provide a \"resolveType\" " . + "function and implementing Type {$this->name} does not provide a " . + '"isTypeOf" function. There is no way to resolve this implementing ' . + 'type during execution.' + ); + } + } + } } diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index b094e29..70c516f 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -237,6 +237,13 @@ abstract class Type implements \JsonSerializable return null; } + /** + * @throws InvariantViolation + */ + public function assertValid() + { + } + /** * @return string */ diff --git a/src/Type/Definition/UnionType.php b/src/Type/Definition/UnionType.php index b6fe42c..fd1a79e 100644 --- a/src/Type/Definition/UnionType.php +++ b/src/Type/Definition/UnionType.php @@ -1,6 +1,7 @@ types) { - if ($this->config['types'] instanceof \Closure) { + if (!isset($this->config['types'])) { + $types = null; + } else if (is_callable($this->config['types'])) { $types = call_user_func($this->config['types']); } else { $types = $this->config['types']; } - Utils::invariant( - is_array($types), - 'Option "types" of union "%s" is expected to return array of types (or closure returning array of types)', - $this->name - ); + if (!is_array($types)) { + throw new InvariantViolation( + "{$this->name} types must be an Array or a callable which returns an Array." + ); + } $this->types = []; foreach ($types as $type) { @@ -123,4 +126,48 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType } return null; } + + /** + * @throws InvariantViolation + */ + public function assertValid() + { + parent::assertValid(); + + $types = $this->getTypes(); + Utils::invariant( + !empty($types), + "{$this->name} types must not be empty" + ); + + if (isset($this->config['resolveType'])) { + Utils::invariant( + is_callable($this->config['resolveType']), + "{$this->name} must provide \"resolveType\" as a function." + ); + } + + $includedTypeNames = []; + foreach ($types as $objType) { + Utils::invariant( + $objType instanceof ObjectType, + "{$this->name} may only contain Object types, it cannot contain: %s.", + Utils::printSafe($objType) + ); + Utils::invariant( + !isset($includedTypeNames[$objType->name]), + "{$this->name} can include {$objType->name} type only once." + ); + $includedTypeNames[$objType->name] = true; + if (!isset($this->config['resolveType'])) { + Utils::invariant( + isset($objType->config['isTypeOf']) && is_callable($objType->config['isTypeOf']), + "Union type \"{$this->name}\" does not provide a \"resolveType\" " . + "function and possible type \"{$objType->name}\" does not provide an " . + '"isTypeOf" function. There is no way to resolve this possible type ' . + 'during execution.' + ); + } + } + } } diff --git a/src/Type/Schema.php b/src/Type/Schema.php index d9fd9d1..4909e49 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -390,4 +390,111 @@ class Schema } return $this->resolvedTypes[$typeName]; } + + + /** + * @throws InvariantViolation + */ + 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) + ); + } + + foreach ($this->getTypeMap() as $name => $type) { + $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); + } + } + } + } + + 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 a6b3688..07efb3c 100644 --- a/src/Type/SchemaConfig.php +++ b/src/Type/SchemaConfig.php @@ -1,6 +1,7 @@ directives; + return $this->directives ?: []; } /** diff --git a/src/Utils/TypeComparators.php b/src/Utils/TypeComparators.php new file mode 100644 index 0000000..64639b4 --- /dev/null +++ b/src/Utils/TypeComparators.php @@ -0,0 +1,137 @@ +getWrappedType(), $typeB->getWrappedType()); + } + + // If either type is a list, the other must also be a list. + if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) { + return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); + } + + // Otherwise the types are not equal. + return false; + } + + /** + * Provided a type and a super type, return true if the first type is either + * equal or a subset of the second super type (covariant). + * + * @param Schema $schema + * @param Type $maybeSubType + * @param Type $superType + * @return bool + */ + static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType) + { + // Equivalent type is a valid subtype + if ($maybeSubType === $superType) { + return true; + } + + // If superType is non-null, maybeSubType must also be nullable. + if ($superType instanceof NonNull) { + if ($maybeSubType instanceof NonNull) { + return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType()); + } + return false; + } else if ($maybeSubType instanceof NonNull) { + // If superType is nullable, maybeSubType may be non-null. + return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType); + } + + // If superType type is a list, maybeSubType type must also be a list. + if ($superType instanceof ListOfType) { + if ($maybeSubType instanceof ListOfType) { + return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType()); + } + return false; + } else if ($maybeSubType instanceof ListOfType) { + // If superType is not a list, maybeSubType must also be not a list. + return false; + } + + // If superType type is an abstract type, maybeSubType type may be a currently + // possible object type. + if (Type::isAbstractType($superType) && $maybeSubType instanceof ObjectType && $schema->isPossibleType($superType, $maybeSubType)) { + return true; + } + + // Otherwise, the child type is not a valid subtype of the parent type. + return false; + } + + /** + * Provided two composite types, determine if they "overlap". Two composite + * types overlap when the Sets of possible concrete types for each intersect. + * + * This is often used to determine if a fragment of a given type could possibly + * be visited in a context of another type. + * + * This function is commutative. + * + * @param Schema $schema + * @param CompositeType $typeA + * @param CompositeType $typeB + * @return bool + */ + static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB) + { + // Equivalent types overlap + if ($typeA === $typeB) { + return true; + } + + if ($typeA instanceof AbstractType) { + if ($typeB instanceof AbstractType) { + // If both types are abstract, then determine if there is any intersection + // between possible concrete types of each. + foreach ($schema->getPossibleTypes($typeA) as $type) { + if ($schema->isPossibleType($typeB, $type)) { + return true; + } + } + return false; + } + + /** @var $typeB ObjectType */ + // Determine if the latter type is a possible concrete type of the former. + return $schema->isPossibleType($typeA, $typeB); + } + + if ($typeB instanceof AbstractType) { + /** @var $typeA ObjectType */ + // Determine if the former type is a possible concrete type of the latter. + return $schema->isPossibleType($typeB, $typeA); + } + + // Otherwise the types do not overlap. + return false; + } +} diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index 3f02b72..b1fc809 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -7,8 +7,7 @@ use GraphQL\Language\AST\NamedTypeNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NonNullTypeNode; -use GraphQL\Schema; -use GraphQL\Type\Definition\AbstractType; +use GraphQL\Type\Schema; use GraphQL\Type\Definition\CompositeType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; @@ -32,114 +31,27 @@ use GraphQL\Type\Introspection; class TypeInfo { /** - * Provided two types, return true if the types are equal (invariant). + * @deprecated moved to GraphQL\Utils\TypeComparators */ public static function isEqualType(Type $typeA, Type $typeB) { - // Equivalent types are equal. - if ($typeA === $typeB) { - return true; - } - - // If either type is non-null, the other must also be non-null. - if ($typeA instanceof NonNull && $typeB instanceof NonNull) { - return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); - } - - // If either type is a list, the other must also be a list. - if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) { - return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); - } - - // Otherwise the types are not equal. - return false; + return TypeComparators::isEqualType($typeA, $typeB); } /** - * Provided a type and a super type, return true if the first type is either - * equal or a subset of the second super type (covariant). + * @deprecated moved to GraphQL\Utils\TypeComparators */ static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType) { - // Equivalent type is a valid subtype - if ($maybeSubType === $superType) { - return true; - } - - // If superType is non-null, maybeSubType must also be nullable. - if ($superType instanceof NonNull) { - if ($maybeSubType instanceof NonNull) { - return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType()); - } - return false; - } else if ($maybeSubType instanceof NonNull) { - // If superType is nullable, maybeSubType may be non-null. - return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType); - } - - // If superType type is a list, maybeSubType type must also be a list. - if ($superType instanceof ListOfType) { - if ($maybeSubType instanceof ListOfType) { - return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType()); - } - return false; - } else if ($maybeSubType instanceof ListOfType) { - // If superType is not a list, maybeSubType must also be not a list. - return false; - } - - // If superType type is an abstract type, maybeSubType type may be a currently - // possible object type. - if (Type::isAbstractType($superType) && $maybeSubType instanceof ObjectType && $schema->isPossibleType($superType, $maybeSubType)) { - return true; - } - - // Otherwise, the child type is not a valid subtype of the parent type. - return false; + return TypeComparators::isTypeSubTypeOf($schema, $maybeSubType, $superType); } - /** - * Provided two composite types, determine if they "overlap". Two composite - * types overlap when the Sets of possible concrete types for each intersect. - * - * This is often used to determine if a fragment of a given type could possibly - * be visited in a context of another type. - * - * This function is commutative. + * @deprecated moved to GraphQL\Utils\TypeComparators */ static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB) { - // Equivalent types overlap - if ($typeA === $typeB) { - return true; - } - - if ($typeA instanceof AbstractType) { - if ($typeB instanceof AbstractType) { - // If both types are abstract, then determine if there is any intersection - // between possible concrete types of each. - foreach ($schema->getPossibleTypes($typeA) as $type) { - if ($schema->isPossibleType($typeB, $type)) { - return true; - } - } - return false; - } - - /** @var $typeB ObjectType */ - // Determine if the latter type is a possible concrete type of the former. - return $schema->isPossibleType($typeA, $typeB); - } - - if ($typeB instanceof AbstractType) { - /** @var $typeA ObjectType */ - // Determine if the former type is a possible concrete type of the latter. - return $schema->isPossibleType($typeB, $typeA); - } - - // Otherwise the types do not overlap. - return false; + return TypeComparators::doTypesOverlap($schema, $typeA, $typeB); } diff --git a/src/Utils/Utils.php b/src/Utils/Utils.php index ee518c6..be96621 100644 --- a/src/Utils/Utils.php +++ b/src/Utils/Utils.php @@ -218,7 +218,7 @@ class Utils * @param string $message * @param mixed $sprintfParam1 * @param mixed $sprintfParam2 ... - * @throws \Exception + * @throws InvariantViolation */ public static function invariant($test, $message = '') { @@ -420,7 +420,7 @@ class Utils /** * @param $name * @param bool $isIntrospection - * @throws Error + * @throws InvariantViolation */ public static function assertValidName($name, $isIntrospection = false) { diff --git a/src/Validator/Rules/VariablesInAllowedPosition.php b/src/Validator/Rules/VariablesInAllowedPosition.php index 86c0e2a..e29ac57 100644 --- a/src/Validator/Rules/VariablesInAllowedPosition.php +++ b/src/Validator/Rules/VariablesInAllowedPosition.php @@ -7,6 +7,7 @@ use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; +use GraphQL\Utils\TypeComparators; use GraphQL\Utils\TypeInfo; use GraphQL\Validator\ValidationContext; @@ -45,7 +46,7 @@ class VariablesInAllowedPosition $schema = $context->getSchema(); $varType = TypeInfo::typeFromAST($schema, $varDef->type); - if ($varType && !TypeInfo::isTypeSubTypeOf($schema, $this->effectiveType($varType, $varDef), $type)) { + if ($varType && !TypeComparators::isTypeSubTypeOf($schema, $this->effectiveType($varType, $varDef), $type)) { $context->reportError(new Error( self::badVarPosMessage($varName, $varType, $type), [$varDef, $node] diff --git a/tests/Executor/LazyInterfaceTest.php b/tests/Executor/LazyInterfaceTest.php index d181d28..8dc05d8 100644 --- a/tests/Executor/LazyInterfaceTest.php +++ b/tests/Executor/LazyInterfaceTest.php @@ -62,12 +62,12 @@ class LazyInterfaceTest extends \PHPUnit_Framework_TestCase if (!$this->lazyInterface) { $this->lazyInterface = new InterfaceType([ 'name' => 'LazyInterface', + 'fields' => [ + 'a' => Type::string() + ], 'resolveType' => function() { return $this->getTestObjectType(); }, - 'resolve' => function() { - return []; - } ]); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 984c858..426fc93 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -47,7 +47,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase public function testSchemaDefinition() { - $mutationType = $queryType = $subscriptionType = new ObjectType(['name' => 'A', 'fields' => []]); + $mutationType = $queryType = $subscriptionType = new ObjectType(['name' => 'A', 'fields' => ['a' => Type::string()]]); $schema = new Schema([ 'query' => $queryType @@ -283,7 +283,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase public function testValidate() { $server = Server::create() - ->setQueryType(new ObjectType(['name' => 'Q', 'fields' => []])); + ->setQueryType(new ObjectType(['name' => 'Q', 'fields' => ['a' => Type::string()]])); $ast = $server->parse('{q}'); $errors = $server->validate($ast); diff --git a/tests/Type/DefinitionTest.php b/tests/Type/DefinitionTest.php index 6ee89ed..b84308d 100644 --- a/tests/Type/DefinitionTest.php +++ b/tests/Type/DefinitionTest.php @@ -243,11 +243,11 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase $value = $enumTypeWithDeprecatedValue->getValues()[0]; - $this->assertEquals([ + $this->assertArraySubset([ 'name' => 'foo', 'description' => null, 'deprecationReason' => 'Just because', - 'value' => 'foo' + 'value' => 'foo', ], (array) $value); $this->assertEquals(true, $value->isDeprecated()); @@ -284,8 +284,8 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase $actual = $EnumTypeWithNullishValue->getValues(); $this->assertEquals(count($expected), count($actual)); - $this->assertEquals($expected[0], (array)$actual[0]); - $this->assertEquals($expected[1], (array)$actual[1]); + $this->assertArraySubset($expected[0], (array)$actual[0]); + $this->assertArraySubset($expected[1], (array)$actual[1]); } /** diff --git a/tests/Type/IntrospectionTest.php b/tests/Type/IntrospectionTest.php index fcab04d..1026dbb 100644 --- a/tests/Type/IntrospectionTest.php +++ b/tests/Type/IntrospectionTest.php @@ -24,7 +24,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase $emptySchema = new Schema([ 'query' => new ObjectType([ 'name' => 'QueryRoot', - 'fields' => [] + 'fields' => ['a' => Type::string()] ]) ]); @@ -42,938 +42,936 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase ), 'types' => array ( - 0 => - array ( - 'kind' => 'OBJECT', - 'name' => 'QueryRoot', - 'inputFields' => NULL, - 'interfaces' => - array ( + array ( + 'kind' => 'OBJECT', + 'name' => 'QueryRoot', + 'inputFields' => NULL, + 'interfaces' => + array ( + ), + 'enumValues' => NULL, + 'possibleTypes' => NULL, + 'fields' => array ( + array ( + 'name' => 'a', + 'args' => array(), + 'type' => array( + 'kind' => 'SCALAR', + 'name' => 'String', + 'ofType' => null ), - 'enumValues' => NULL, - 'possibleTypes' => NULL, - 'fields' => Array () - ), - 1 => - array ( - 'kind' => 'OBJECT', - 'name' => '__Schema', - 'fields' => - array ( - 0 => - array ( - 'name' => 'types', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'LIST', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__Type' - ), - ), - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 1 => - array ( - 'name' => 'queryType', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__Type', - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 2 => - array ( - 'name' => 'mutationType', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'OBJECT', - 'name' => '__Type', - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 3 => - array ( - 'name' => 'subscriptionType', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'OBJECT', - 'name' => '__Type', - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 4 => - array ( - 'name' => 'directives', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'LIST', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__Directive', - ), - ), - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - ), - 'inputFields' => NULL, - 'interfaces' => - array ( - ), - 'enumValues' => NULL, - 'possibleTypes' => NULL, - ), - 2 => - array ( - 'kind' => 'OBJECT', - 'name' => '__Type', - 'fields' => - array ( - 0 => - array ( - 'name' => 'kind', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'ENUM', - 'name' => '__TypeKind', - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 1 => - array ( - 'name' => 'name', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 2 => - array ( - 'name' => 'description', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 3 => - array ( - 'name' => 'fields', - 'args' => - array ( - 0 => - array ( - 'name' => 'includeDeprecated', - 'type' => - array ( - 'kind' => 'SCALAR', - 'name' => 'Boolean', - ), - 'defaultValue' => 'false', - ), - ), - 'type' => - array ( - 'kind' => 'LIST', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__Field', - ), - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 4 => - array ( - 'name' => 'interfaces', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'LIST', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__Type', - ), - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 5 => - array ( - 'name' => 'possibleTypes', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'LIST', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__Type', - ), - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 6 => - array ( - 'name' => 'enumValues', - 'args' => - array ( - 0 => - array ( - 'name' => 'includeDeprecated', - 'type' => - array ( - 'kind' => 'SCALAR', - 'name' => 'Boolean', - ), - 'defaultValue' => 'false', - ), - ), - 'type' => - array ( - 'kind' => 'LIST', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__EnumValue', - ), - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 7 => - array ( - 'name' => 'inputFields', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'LIST', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__InputValue', - ), - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 8 => - array ( - 'name' => 'ofType', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'OBJECT', - 'name' => '__Type', - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - ), - 'inputFields' => NULL, - 'interfaces' => - array ( - ), - 'enumValues' => NULL, - 'possibleTypes' => NULL, - ), - 3 => - array ( - 'kind' => 'ENUM', - 'name' => '__TypeKind', - 'fields' => NULL, - 'inputFields' => NULL, - 'interfaces' => NULL, - 'enumValues' => - array ( - 0 => - array ( - 'name' => 'SCALAR', - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 1 => - array ( - 'name' => 'OBJECT', - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 2 => - array ( - 'name' => 'INTERFACE', - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 3 => - array ( - 'name' => 'UNION', - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 4 => - array ( - 'name' => 'ENUM', - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 5 => - array ( - 'name' => 'INPUT_OBJECT', - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 6 => - array ( - 'name' => 'LIST', - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 7 => - array ( - 'name' => 'NON_NULL', - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - ), - 'possibleTypes' => NULL, - ), - 4 => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - 'fields' => NULL, - 'inputFields' => NULL, - 'interfaces' => NULL, - 'enumValues' => NULL, - 'possibleTypes' => NULL, - ), - 5 => - array ( - 'kind' => 'SCALAR', - 'name' => 'Boolean', - 'fields' => NULL, - 'inputFields' => NULL, - 'interfaces' => NULL, - 'enumValues' => NULL, - 'possibleTypes' => NULL, - ), - 6 => - array ( - 'kind' => 'OBJECT', - 'name' => '__Field', - 'fields' => - array ( - 0 => - array ( - 'name' => 'name', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 1 => - array ( - 'name' => 'description', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 2 => - array ( - 'name' => 'args', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'LIST', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__InputValue', - ), - ), - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 3 => - array ( - 'name' => 'type', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__Type', - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 4 => - array ( - 'name' => 'isDeprecated', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'Boolean', - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 5 => - array ( - 'name' => 'deprecationReason', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - ), - 'inputFields' => NULL, - 'interfaces' => - array ( - ), - 'enumValues' => NULL, - 'possibleTypes' => NULL, - ), - 7 => - array ( - 'kind' => 'OBJECT', - 'name' => '__InputValue', - 'fields' => - array ( - 0 => - array ( - 'name' => 'name', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 1 => - array ( - 'name' => 'description', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 2 => - array ( - 'name' => 'type', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__Type', - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 3 => - array ( - 'name' => 'defaultValue', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - ), - 'inputFields' => NULL, - 'interfaces' => - array ( - ), - 'enumValues' => NULL, - 'possibleTypes' => NULL, - ), - 8 => - array ( - 'kind' => 'OBJECT', - 'name' => '__EnumValue', - 'fields' => - array ( - 0 => - array ( - 'name' => 'name', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 1 => - array ( - 'name' => 'description', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 2 => - array ( - 'name' => 'isDeprecated', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'Boolean', - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 3 => - array ( - 'name' => 'deprecationReason', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - ), - 'inputFields' => NULL, - 'interfaces' => - array ( - ), - 'enumValues' => NULL, - 'possibleTypes' => NULL, - ), - 9 => - array ( - 'kind' => 'OBJECT', - 'name' => '__Directive', - 'fields' => - array ( - 0 => - array ( - 'name' => 'name', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 1 => - array ( - 'name' => 'description', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 2 => - array ( - 'name' => 'locations', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'LIST', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'ENUM', - 'name' => '__DirectiveLocation', - ), - ), - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 3 => - array ( - 'name' => 'args', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'LIST', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__InputValue', - ), - ), - ), - ), - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 4 => - array ( - 'name' => 'onOperation', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'Boolean', - ), - ), - 'isDeprecated' => true, - 'deprecationReason' => 'Use `locations`.', - ), - 5 => - array ( - 'name' => 'onFragment', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'Boolean', - ), - ), - 'isDeprecated' => true, - 'deprecationReason' => 'Use `locations`.', - ), - 6 => - array ( - 'name' => 'onField', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'Boolean', + 'isDeprecated' => false, + 'deprecationReason' => null, + ) + ) + ), + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + 'fields' => NULL, + 'inputFields' => NULL, + 'interfaces' => NULL, + 'enumValues' => NULL, + 'possibleTypes' => NULL, + ), + array ( + 'kind' => 'OBJECT', + 'name' => '__Schema', + 'fields' => + array ( + 0 => + array ( + 'name' => 'types', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__Type' + ), + ), + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 1 => + array ( + 'name' => 'queryType', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__Type', + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + array ( + 'name' => 'mutationType', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'OBJECT', + 'name' => '__Type', + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + array ( + 'name' => 'subscriptionType', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'OBJECT', + 'name' => '__Type', + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + array ( + 'name' => 'directives', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__Directive', + ), + ), + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + ), + 'inputFields' => NULL, + 'interfaces' => + array ( + ), + 'enumValues' => NULL, + 'possibleTypes' => NULL, + ), + array ( + 'kind' => 'OBJECT', + 'name' => '__Type', + 'fields' => + array ( + 0 => + array ( + 'name' => 'kind', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'ENUM', + 'name' => '__TypeKind', + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 1 => + array ( + 'name' => 'name', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 2 => + array ( + 'name' => 'description', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 3 => + array ( + 'name' => 'fields', + 'args' => + array ( + 0 => + array ( + 'name' => 'includeDeprecated', + 'type' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + ), + 'defaultValue' => 'false', + ), + ), + 'type' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__Field', + ), + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 4 => + array ( + 'name' => 'interfaces', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__Type', + ), + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 5 => + array ( + 'name' => 'possibleTypes', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__Type', + ), + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 6 => + array ( + 'name' => 'enumValues', + 'args' => + array ( + 0 => + array ( + 'name' => 'includeDeprecated', + 'type' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + ), + 'defaultValue' => 'false', + ), + ), + 'type' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__EnumValue', + ), + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 7 => + array ( + 'name' => 'inputFields', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__InputValue', + ), + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 8 => + array ( + 'name' => 'ofType', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'OBJECT', + 'name' => '__Type', + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + ), + 'inputFields' => NULL, + 'interfaces' => + array ( + ), + 'enumValues' => NULL, + 'possibleTypes' => NULL, + ), + array ( + 'kind' => 'ENUM', + 'name' => '__TypeKind', + 'fields' => NULL, + 'inputFields' => NULL, + 'interfaces' => NULL, + 'enumValues' => + array ( + 0 => + array ( + 'name' => 'SCALAR', + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 1 => + array ( + 'name' => 'OBJECT', + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 2 => + array ( + 'name' => 'INTERFACE', + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 3 => + array ( + 'name' => 'UNION', + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 4 => + array ( + 'name' => 'ENUM', + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 5 => + array ( + 'name' => 'INPUT_OBJECT', + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 6 => + array ( + 'name' => 'LIST', + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 7 => + array ( + 'name' => 'NON_NULL', + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + ), + 'possibleTypes' => NULL, + ), + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'fields' => NULL, + 'inputFields' => NULL, + 'interfaces' => NULL, + 'enumValues' => NULL, + 'possibleTypes' => NULL, + ), + array ( + 'kind' => 'OBJECT', + 'name' => '__Field', + 'fields' => + array ( + 0 => + array ( + 'name' => 'name', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 1 => + array ( + 'name' => 'description', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 2 => + array ( + 'name' => 'args', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__InputValue', + ), + ), + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 3 => + array ( + 'name' => 'type', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__Type', + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 4 => + array ( + 'name' => 'isDeprecated', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 5 => + array ( + 'name' => 'deprecationReason', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + ), + 'inputFields' => NULL, + 'interfaces' => + array ( + ), + 'enumValues' => NULL, + 'possibleTypes' => NULL, + ), + array ( + 'kind' => 'OBJECT', + 'name' => '__InputValue', + 'fields' => + array ( + 0 => + array ( + 'name' => 'name', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 1 => + array ( + 'name' => 'description', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 2 => + array ( + 'name' => 'type', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__Type', + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 3 => + array ( + 'name' => 'defaultValue', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + ), + 'inputFields' => NULL, + 'interfaces' => + array ( + ), + 'enumValues' => NULL, + 'possibleTypes' => NULL, + ), + array ( + 'kind' => 'OBJECT', + 'name' => '__EnumValue', + 'fields' => + array ( + 0 => + array ( + 'name' => 'name', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 1 => + array ( + 'name' => 'description', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 2 => + array ( + 'name' => 'isDeprecated', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 3 => + array ( + 'name' => 'deprecationReason', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + ), + 'inputFields' => NULL, + 'interfaces' => + array ( + ), + 'enumValues' => NULL, + 'possibleTypes' => NULL, + ), + array ( + 'kind' => 'OBJECT', + 'name' => '__Directive', + 'fields' => + array ( + 0 => + array ( + 'name' => 'name', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 1 => + array ( + 'name' => 'description', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 2 => + array ( + 'name' => 'locations', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'ENUM', + 'name' => '__DirectiveLocation', + ), + ), + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 3 => + array ( + 'name' => 'args', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__InputValue', + ), + ), + ), + ), + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 4 => + array ( + 'name' => 'onOperation', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + ), + ), + 'isDeprecated' => true, + 'deprecationReason' => 'Use `locations`.', + ), + 5 => + array ( + 'name' => 'onFragment', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + ), + ), + 'isDeprecated' => true, + 'deprecationReason' => 'Use `locations`.', + ), + 6 => + array ( + 'name' => 'onField', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', - ), - ), - 'isDeprecated' => true, - 'deprecationReason' => 'Use `locations`.', - ), - ), - 'inputFields' => NULL, - 'interfaces' => - array ( - ), - 'enumValues' => NULL, - 'possibleTypes' => NULL, - ), - 10 => - array ( - 'kind' => 'ENUM', - 'name' => '__DirectiveLocation', - 'fields' => NULL, - 'inputFields' => NULL, - 'interfaces' => NULL, - 'enumValues' => - array ( - 0 => - array ( - 'name' => 'QUERY', - 'isDeprecated' => false, - 'deprecationReason' => null - ), - 1 => - array ( - 'name' => 'MUTATION', - 'isDeprecated' => false, - 'deprecationReason' => null - ), - 2 => - array ( - 'name' => 'SUBSCRIPTION', - 'isDeprecated' => false, - 'deprecationReason' => null - ), - 3 => - array ( - 'name' => 'FIELD', - 'isDeprecated' => false, - 'deprecationReason' => null - ), - 4 => - array ( - 'name' => 'FRAGMENT_DEFINITION', - 'isDeprecated' => false, - 'deprecationReason' => null - ), - 5 => - array ( - 'name' => 'FRAGMENT_SPREAD', - 'isDeprecated' => false, - 'deprecationReason' => null - ), - 6 => - array ( - 'name' => 'INLINE_FRAGMENT', - 'isDeprecated' => false, - 'deprecationReason' => null - ), - ), - 'possibleTypes' => NULL, - ), - 11 => array ( + ), + ), + 'isDeprecated' => true, + 'deprecationReason' => 'Use `locations`.', + ), + ), + 'inputFields' => NULL, + 'interfaces' => + array ( + ), + 'enumValues' => NULL, + 'possibleTypes' => NULL, + ), + array ( + 'kind' => 'ENUM', + 'name' => '__DirectiveLocation', + 'fields' => NULL, + 'inputFields' => NULL, + 'interfaces' => NULL, + 'enumValues' => + array ( + 0 => + array ( + 'name' => 'QUERY', + 'isDeprecated' => false, + 'deprecationReason' => null + ), + 1 => + array ( + 'name' => 'MUTATION', + 'isDeprecated' => false, + 'deprecationReason' => null + ), + 2 => + array ( + 'name' => 'SUBSCRIPTION', + 'isDeprecated' => false, + 'deprecationReason' => null + ), + 3 => + array ( + 'name' => 'FIELD', + 'isDeprecated' => false, + 'deprecationReason' => null + ), + 4 => + array ( + 'name' => 'FRAGMENT_DEFINITION', + 'isDeprecated' => false, + 'deprecationReason' => null + ), + 5 => + array ( + 'name' => 'FRAGMENT_SPREAD', + 'isDeprecated' => false, + 'deprecationReason' => null + ), + 6 => + array ( + 'name' => 'INLINE_FRAGMENT', + 'isDeprecated' => false, + 'deprecationReason' => null + ), + ), + 'possibleTypes' => NULL, + ), + array ( 'kind' => 'SCALAR', 'name' => 'ID', 'fields' => NULL, @@ -982,7 +980,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase 'enumValues' => NULL, 'possibleTypes' => NULL, ), - 12 => array ( + array ( 'kind' => 'SCALAR', 'name' => 'Float', 'fields' => NULL, @@ -991,7 +989,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase 'enumValues' => NULL, 'possibleTypes' => NULL, ), - 13 => array ( + array ( 'kind' => 'SCALAR', 'name' => 'Int', 'fields' => NULL, @@ -1491,7 +1489,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase { $QueryRoot = new ObjectType([ 'name' => 'QueryRoot', - 'fields' => [] + 'fields' => ['a' => Type::string()] ]); $schema = new Schema(['query' => $QueryRoot]); @@ -1551,7 +1549,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase { $QueryRoot = new ObjectType([ 'name' => 'QueryRoot', - 'fields' => [] + 'fields' => ['a' => Type::string()] ]); $schema = new Schema(['query' => $QueryRoot]); diff --git a/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index 6da5718..47cfbd9 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -2,15 +2,130 @@ namespace GraphQL\Tests\Type; use GraphQL\Error\InvariantViolation; -use GraphQL\Schema; +use GraphQL\Error\Warning; +use GraphQL\Type\Definition\Config; +use GraphQL\Type\Schema; +use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; +use GraphQL\Utils\Utils; class ValidationTest extends \PHPUnit_Framework_TestCase { + public $SomeScalarType; + + public $SomeObjectType; + + public $ObjectWithIsTypeOf; + + public $SomeUnionType; + + public $SomeInterfaceType; + + public $SomeEnumType; + + public $SomeInputObjectType; + + public $outputTypes; + + public $notOutputTypes; + + public $inputTypes; + + public $notInputTypes; + + public $String; + + public function setUp() + { + Config::disableValidation(); + + $this->String = 'TestString'; + + $this->SomeScalarType = new CustomScalarType([ + 'name' => 'SomeScalar', + 'serialize' => function() {}, + 'parseValue' => function() {}, + 'parseLiteral' => function() {} + ]); + + $this->SomeObjectType = new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [ 'f' => [ 'type' => Type::string() ] ], + 'interfaces' => function() {return [$this->SomeInterfaceType];} + ]); + + $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', + 'values' => [ + 'ONLY' => [] + ] + ]); + + $this->SomeInputObjectType = new InputObjectType([ + 'name' => 'SomeInputObject', + 'fields' => [ + 'val' => [ 'type' => Type::string(), 'defaultValue' => 'hello' ] + ] + ]); + + $this->outputTypes = $this->withModifiers([ + Type::string(), + $this->SomeScalarType, + $this->SomeEnumType, + $this->SomeObjectType, + $this->SomeUnionType, + $this->SomeInterfaceType + ]); + + $this->notOutputTypes = $this->withModifiers([ + $this->SomeInputObjectType, + ]); + $this->notOutputTypes[] = $this->String; + + $this->inputTypes = $this->withModifiers([ + Type::string(), + $this->SomeScalarType, + $this->SomeEnumType, + $this->SomeInputObjectType, + ]); + + $this->notInputTypes = $this->withModifiers([ + $this->SomeObjectType, + $this->SomeUnionType, + $this->SomeInterfaceType, + ]); + + $this->notInputTypes[] = $this->String; + } + public function testRejectsTypesWithoutNames() { $this->assertEachCallableThrows([ @@ -96,165 +211,2544 @@ class ValidationTest extends \PHPUnit_Framework_TestCase } // 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 + + /** + * @it accepts a Schema whose query type is an object type + */ + public function testAcceptsASchemaWhoseQueryTypeIsAnObjectType() + { + // Must not throw: + $schema = new Schema([ + 'query' => $this->SomeObjectType + ]); + $schema->assertValid(); + } + + /** + * @it TODO: accepts a Schema whose query and mutation types are object types + */ + public function testAcceptsASchemaWhoseQueryAndMutationTypesAreObjectTypes() + { + $mutationType = new ObjectType([ + 'name' => 'Mutation', + 'fields' => [ + 'edit' => ['type' => Type::string()] + ] + ]); + $schema = new Schema([ + 'query' => $this->SomeObjectType, + 'mutation' => $mutationType + ]); + $schema->assertValid(); + } + + /** + * @it accepts a Schema whose query and subscription types are object types + */ + public function testAcceptsASchemaWhoseQueryAndSubscriptionTypesAreObjectTypes() + { + $subscriptionType = new ObjectType([ + 'name' => 'Subscription', + 'fields' => [ + 'subscribe' => ['type' => Type::string()] + ] + ]); + $schema = new Schema([ + 'query' => $this->SomeObjectType, + 'subscription' => $subscriptionType + ]); + $schema->assertValid(); + } + + /** + * @it rejects a Schema without a query type + */ + public function testRejectsASchemaWithoutAQueryType() + { + $this->setExpectedException(InvariantViolation::class, 'Schema query must be Object Type but got: NULL'); + new Schema([]); + } + + /** + * @it rejects a Schema whose query type is an input type + */ + public function testRejectsASchemaWhoseQueryTypeIsAnInputType() + { + $this->setExpectedException( + InvariantViolation::class, + 'Schema query must be Object Type if provided but got: SomeInputObject' + ); + new Schema([ + 'query' => $this->SomeInputObjectType + ]); + } + + /** + * @it rejects a Schema whose mutation type is an input type + */ + public function testRejectsASchemaWhoseMutationTypeIsAnInputType() + { + $this->setExpectedException( + InvariantViolation::class, + 'Schema mutation must be Object Type if provided but got: SomeInputObject' + ); + new Schema([ + 'query' => $this->SomeObjectType, + 'mutation' => $this->SomeInputObjectType + ]); + } + + /** + * @it rejects a Schema whose subscription type is an input type + */ + public function testRejectsASchemaWhoseSubscriptionTypeIsAnInputType() + { + $this->setExpectedException( + InvariantViolation::class, + 'Schema subscription must be Object Type if provided but got: SomeInputObject' + ); + new Schema([ + 'query' => $this->SomeObjectType, + 'subscription' => $this->SomeInputObjectType + ]); + } + + /** + * @it rejects a Schema whose directives are incorrectly typed + */ + public function testRejectsASchemaWhoseDirectivesAreIncorrectlyTyped() + { + $schema = new Schema([ + 'query' => $this->SomeObjectType, + '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".' + ); + + $schema->assertValid(); + } // 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 + /** + * @it rejects a Schema which redefines a built-in type + */ + public function testRejectsASchemaWhichRedefinesABuiltInType() + { + $FakeString = new CustomScalarType([ + 'name' => 'String', + 'serialize' => function() { + return null; + }, + ]); + + $QueryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'normal' => [ 'type' => Type::string() ], + 'fake' => [ 'type' => $FakeString ], + ] + ]); + + $schema = new Schema(['query' => $QueryType]); + + $this->setExpectedException( + InvariantViolation::class, + 'Schema must contain unique named types but contains multiple types named "String".' + ); + $schema->assertValid(); + } + + /** + * @it rejects a Schema which defines an object type twice + */ + public function testRejectsASchemaWhichDfinesAnObjectTypeTwice() + { + $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 ] + ] + ]); + + $schema = new Schema([ 'query' => $QueryType ]); + + $this->setExpectedException( + InvariantViolation::class, + 'Schema must contain unique named types but contains multiple types named "SameName".' + ); + + $schema->assertValid(); + } + + /** + * @it rejects a Schema which have same named objects implementing an interface + */ + public function testRejectsASchemaWhichHaveSameNamedObjectsImplementingAnInterface() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function() {}, + '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 ], + ] + ]); + + $schema = new Schema([ + 'query' => $QueryType, + 'types' => [ $FirstBadObject, $SecondBadObject ] + ]); + + $this->setExpectedException( + InvariantViolation::class, + 'Schema must contain unique named types but contains multiple types named "BadObject".' + ); + + $schema->assertValid(); + } + // DESCRIBE: Type System: Objects must have fields - // TODO: accepts an Object type with fields object - // TODO: accepts an Object type with a field function + /** + * @it accepts an Object type with fields object + */ + public function testAcceptsAnObjectTypeWithFieldsObject() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [ + 'f' => [ 'type' => Type::string() ] + ] + ])); - // 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 + // Should not throw: + $schema->assertValid(); + } + + /** + * @it accepts an Object type with a field function + */ + public function testAcceptsAnObjectTypeWithAfieldFunction() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => function() { + return [ + 'f' => ['type' => Type::string()] + ]; + } + ])); + $schema->assertValid(); + } + + /** + * @it rejects an Object type with missing fields + */ + public function testRejectsAnObjectTypeWithMissingFields() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject' + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeObject fields must not be empty' + ); + $schema->assertValid(); + } + + /** + * @it rejects an Object type field with undefined config + */ + public function testRejectsAnObjectTypeFieldWithUndefinedConfig() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [ + 'f' => null + ] + ])); + $this->setExpectedException( + InvariantViolation::class, + 'SomeObject.f field config must be an array, but got' + ); + $schema->assertValid(); + } + + /** + * @it rejects an Object type with incorrectly named fields + */ + public function testRejectsAnObjectTypeWithIncorrectlyNamedFields() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [ + 'bad-name-with-dashes' => ['type' => Type::string()] + ] + ])); + + $this->setExpectedException( + InvariantViolation::class + ); + + $schema->assertValid(); + } + + /** + * @it warns about an Object type with reserved named fields + */ + public function testWarnsAboutAnObjectTypeWithReservedNamedFields() + { + $lastMessage = null; + Warning::setWarningHandler(function($message) use (&$lastMessage) { + $lastMessage = $message; + }); + + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [ + '__notPartOfIntrospection' => ['type' => Type::string()] + ] + ])); + + $schema->assertValid(); + + $this->assertEquals( + 'Name "__notPartOfIntrospection" must not begin with "__", which is reserved by GraphQL introspection. '. + 'In a future release of graphql this will become an exception', + $lastMessage + ); + Warning::setWarningHandler(null); + } + + public function testAcceptsShorthandNotationForFields() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [ + 'field' => Type::string() + ] + ])); + $schema->assertValid(); + } + + /** + * @it rejects an Object type with incorrectly typed fields + */ + public function testRejectsAnObjectTypeWithIncorrectlyTypedFields() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [ + 'field' => new \stdClass(['type' => Type::string()]) + ] + ])); + + // FIXME: this exception message is caused by old mechanism of type resolution (see #35), + // this has to be changed as soon as we drop this mechanism + $this->setExpectedException( + InvariantViolation::class, + 'Expecting instance of GraphQL\Type\Definition\Type, got "stdClass' + ); + $schema->assertValid(); + } + + /** + * @it rejects an Object type with empty fields + */ + public function testRejectsAnObjectTypeWithEmptyFields() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [] + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeObject fields must not be empty' + ); + $schema->assertValid(); + } + + /** + * @it rejects an Object type with a field function that returns nothing + */ + public function testRejectsAnObjectTypeWithAFieldFunctionThatReturnsNothing() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => function() {} + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeObject fields must be an array or a callable which returns such an array.' + ); + $schema->assertValid(); + } + + /** + * @it rejects an Object type with a field function that returns empty + */ + public function testRejectsAnObjectTypeWithAFieldFunctionThatReturnsEmpty() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => function() { + return []; + } + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeObject fields must not be empty' + ); + $schema->assertValid(); + } // DESCRIBE: Type System: Fields args must be properly named - // TODO: accepts field args with valid names - // TODO: rejects field arg with invalid names + + /** + * @it accepts field args with valid names + */ + public function testAcceptsFieldArgsWithValidNames() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [ + 'goodField' => [ + 'type' => Type::string(), + 'args' => [ + 'goodArg' => ['type' => Type::string()] + ] + ] + ] + ])); + $schema->assertValid(); + } + + /** + * @it rejects field arg with invalid names + */ + public function testRejectsFieldArgWithInvalidNames() + { + $QueryType = new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [ + 'badField' => [ + 'type' => Type::string(), + 'args' => [ + 'bad-name-with-dashes' => ['type' => Type::string()] + ] + ] + ] + ]); + $schema = new Schema(['query' => $QueryType]); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeObject.badField(bad-name-with-dashes:) Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "bad-name-with-dashes" does not.' + ); + + $schema->assertValid(); + } // 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 + + /** + * @it accepts an Object type with field args + */ + public function testAcceptsAnObjectTypeWithFieldArgs() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [ + 'goodField' => [ + 'type' => Type::string(), + 'args' => [ + 'goodArg' => ['type' => Type::string()] + ] + ] + ] + ])); + $schema->assertValid(); + } + + /** + * @it rejects an Object type with incorrectly typed field args + */ + public function testRejectsAnObjectTypeWithIncorrectlyTypedFieldArgs() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [ + 'badField' => [ + 'type' => Type::string(), + 'args' => [ + ['badArg' => Type::string()] + ] + ] + ] + ])); + + // FIXME + $this->setExpectedException( + InvariantViolation::class, + 'Expecting instance of GraphQL\Type\Definition\Type, got "NULL"' + ); + + $schema->assertValid(); + } // 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 + + /** + * @it accepts an Object type with array interfaces + */ + public function testAcceptsAnObjectTypeWithArrayInterfaces() + { + $AnotherInterfaceType = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => ['f' => ['type' => Type::string()]] + ]); + + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'interfaces' => [$AnotherInterfaceType], + 'fields' => ['f' => ['type' => Type::string()]] + ])); + $schema->assertValid(); + } + + /** + * @it accepts an Object type with interfaces as a function returning an array + */ + public function testAcceptsAnObjectTypeWithInterfacesAsAFunctionReturningAnArray() + { + $AnotherInterfaceType = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => ['f' => ['type' => Type::string()]] + ]); + + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'interfaces' => function () use ($AnotherInterfaceType) { + return [$AnotherInterfaceType]; + }, + 'fields' => ['f' => ['type' => Type::string()]] + ])); + $schema->assertValid(); + } + + /** + * @it rejects an Object type with incorrectly typed interfaces + */ + public function testRejectsAnObjectTypeWithIncorrectlyTypedInterfaces() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'interfaces' => new \stdClass(), + 'fields' => ['f' => ['type' => Type::string()]] + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeObject interfaces must be an Array or a callable which returns an Array.' + ); + + $schema->assertValid(); + } + + /** + * @it rejects an Object that declare it implements same interface more than once + */ + public function testRejectsAnObjectThatDeclareItImplementsSameInterfaceMoreThanOnce() + { + $NonUniqInterface = new InterfaceType([ + 'name' => 'NonUniqInterface', + 'resolveType' => function () { + }, + 'fields' => ['f' => ['type' => Type::string()]], + ]); + + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function(){}, + 'fields' => ['f' => ['type' => Type::string()]], + ]); + + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'interfaces' => function () use ($NonUniqInterface, $AnotherInterface, $NonUniqInterface) { + 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 + */ + public function testRejectsAnObjectTypeWithInterfacesAsAFunctionReturningAnIncorrectType() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'interfaces' => function () { + return new \stdClass(); + }, + 'fields' => ['f' => ['type' => Type::string()]] + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeObject interfaces must be an Array or a callable which returns an Array.' + ); + + $schema->assertValid(); + } + // 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 + + /** + * @it accepts a Union type with array types + */ + public function testAcceptsAUnionTypeWithArrayTypes() + { + $schema = $this->schemaWithFieldType(new UnionType([ + 'name' => 'SomeUnion', + 'resolveType' => function () { + return null; + }, + 'types' => [$this->SomeObjectType], + ])); + $schema->assertValid(); + } + + /** + * @it accepts a Union type with function returning an array of types + */ + public function testAcceptsAUnionTypeWithFunctionReturningAnArrayOfTypes() + { + $schema = $this->schemaWithFieldType(new UnionType([ + 'name' => 'SomeUnion', + 'resolveType' => function () { + return null; + }, + 'types' => function () { + return [$this->SomeObjectType]; + }, + ])); + $schema->assertValid(); + } + + /** + * @it rejects a Union type without types + */ + public function testRejectsAUnionTypeWithoutTypes() + { + $schema = $this->schemaWithFieldType(new UnionType([ + 'name' => 'SomeUnion', + 'resolveType' => function() {return null;} + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeUnion types must be an Array or a callable which returns an Array.' + ); + + $schema->assertValid(); + } + + /** + * @it rejects a Union type with empty types + */ + public function testRejectsAUnionTypeWithemptyTypes() + { + $schema = $this->schemaWithFieldType(new UnionType([ + 'name' => 'SomeUnion', + 'resolveType' => function () { + }, + 'types' => [] + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeUnion types must not be empty' + ); + $schema->assertValid(); + } + + /** + * @it rejects a Union type with incorrectly typed types + */ + public function testRejectsAUnionTypeWithIncorrectlyTypedTypes() + { + $schema = $this->schemaWithFieldType(new UnionType([ + 'name' => 'SomeUnion', + 'resolveType' => function () { + }, + 'types' => $this->SomeObjectType + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeUnion types must be an Array or a callable which returns an Array.' + ); + $schema->assertValid(); + } + + /** + * @it rejects a Union type with duplicated member type + */ + public function testRejectsAUnionTypeWithDuplicatedMemberType() + { + $schema = $this->schemaWithFieldType(new UnionType([ + 'name' => 'SomeUnion', + 'resolveType' => function(){}, + 'types' => [ + $this->SomeObjectType, + $this->SomeObjectType, + ], + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeUnion can include SomeObject type only once.' + ); + $schema->assertValid(); + } // 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 + + /** + * @it accepts an Input Object type with fields + */ + public function testAcceptsAnInputObjectTypeWithFields() + { + $schema = $this->schemaWithInputObject(new InputObjectType([ + 'name' => 'SomeInputObject', + 'fields' => [ + 'f' => ['type' => Type::string()] + ] + ])); + + $schema->assertValid(); + } + + /** + * @it accepts an Input Object type with a field function + */ + public function testAcceptsAnInputObjectTypeWithAFieldFunction() + { + $schema = $this->schemaWithInputObject(new InputObjectType([ + 'name' => 'SomeInputObject', + 'fields' => function () { + return [ + 'f' => ['type' => Type::string()] + ]; + } + ])); + + $schema->assertValid(); + } + + /** + * @it rejects an Input Object type with missing fields + */ + public function testRejectsAnInputObjectTypeWithMissingFields() + { + $schema = $this->schemaWithInputObject(new InputObjectType([ + 'name' => 'SomeInputObject', + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeInputObject fields must not be empty' + ); + $schema->assertValid(); + } + + /** + * @it rejects an Input Object type with incorrectly typed fields + */ + public function testRejectsAnInputObjectTypeWithIncorrectlyTypedFields() + { + $schema = $this->schemaWithInputObject(new InputObjectType([ + 'name' => 'SomeInputObject', + 'fields' => [ + ['field' => Type::string()] + ] + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'Expecting instance of GraphQL\Type\Definition\Type, got "NULL"' + ); + $schema->assertValid(); + } + + /** + * @it rejects an Input Object type with empty fields + */ + public function testRejectsAnInputObjectTypeWithEmptyFields() + { + $schema = $this->schemaWithInputObject(new InputObjectType([ + 'name' => 'SomeInputObject', + 'fields' => new \stdClass() + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeInputObject fields must be an array or a callable which returns such an array.' + ); + $schema->assertValid(); + } + + /** + * @it rejects an Input Object type with a field function that returns nothing + */ + public function testRejectsAnInputObjectTypeWithAFieldFunctionThatReturnsNothing() + { + $schema = $this->schemaWithInputObject(new ObjectType([ + 'name' => 'SomeInputObject', + 'fields' => function () { + } + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeInputObject fields must be an array or a callable which returns such an array.' + ); + + $schema->assertValid(); + } + + /** + * @it rejects an Input Object type with a field function that returns empty + */ + public function testRejectsAnInputObjectTypeWithAFieldFunctionThatReturnsEmpty() + { + $schema = $this->schemaWithInputObject(new InputObjectType([ + 'name' => 'SomeInputObject', + 'fields' => function () { + return new \stdClass(); + } + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeInputObject fields must be an array or a callable which returns such an array.' + ); + + $schema->assertValid(); + } + + // DESCRIBE: Type System: Input Object fields must not have resolvers + + /** + * @it accepts an Input Object type with no resolver + */ + public function testAcceptsAnInputObjectTypeWithNoResolver() + { + $schema = $this->schemaWithInputObject(new InputObjectType([ + 'name' => 'SomeInputObject', + 'fields' => [ + 'f' => [ + 'type' => Type::string(), + ] + ] + ])); + + $schema->assertValid(); + } + + /** + * @it accepts an Input Object type with null resolver + */ + public function testAcceptsAnInputObjectTypeWithNullResolver() + { + $schema = $this->schemaWithInputObject(new InputObjectType([ + 'name' => 'SomeInputObject', + 'fields' => [ + 'f' => [ + 'type' => Type::string(), + 'resolve' => null, + ] + ] + ])); + $schema->assertValid(); + } + + /** + * @it rejects an Input Object type with resolver function + */ + public function testRejectsAnInputObjectTypeWithResolverFunction() + { + $schema = $this->schemaWithInputObject(new InputObjectType([ + 'name' => 'SomeInputObject', + 'fields' => [ + 'f' => [ + 'type' => Type::string(), + 'resolve' => function () { + return 0; + }, + ] + ] + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeInputObject.f field type has a resolve property, but Input Types cannot define resolvers.' + ); + $schema->assertValid(); + } + + /** + * @it rejects an Input Object type with resolver constant + */ + public function testRejectsAnInputObjectTypeWithResolverConstant() + { + $schema = $this->schemaWithInputObject(new InputObjectType([ + 'name' => 'SomeInputObject', + 'fields' => [ + 'f' => [ + 'type' => Type::string(), + 'resolve' => new \stdClass(), + ] + ] + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeInputObject.f field type has a resolve property, but Input Types cannot define resolvers.' + ); + $schema->assertValid(); + } + // 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 + + /** + * @it accepts an Object type with an isTypeOf function + */ + public function testAcceptsAnObjectTypeWithAnIsTypeOfFunction() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'AnotherObject', + 'isTypeOf' => function () { + return true; + }, + 'fields' => ['f' => ['type' => Type::string()]] + ])); + $schema->assertValid(); + } + + /** + * @it rejects an Object type with an incorrect type for isTypeOf + */ + public function testRejectsAnObjectTypeWithAnIncorrectTypeForIsTypeOf() + { + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'AnotherObject', + 'isTypeOf' => new \stdClass(), + 'fields' => ['f' => ['type' => Type::string()]] + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'AnotherObject must provide \'isTypeOf\' as a function' + ); + + $schema->assertValid(); + } // 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 + + /** + * @it accepts an Interface type defining resolveType + */ + public function testAcceptsAnInterfaceTypeDefiningResolveType() + { + $AnotherInterfaceType = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => ['f' => ['type' => Type::string()]] + ]); + + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'interfaces' => [$AnotherInterfaceType], + 'fields' => ['f' => ['type' => Type::string()]] + ])); + $schema->assertValid(); + } + + /** + * @it accepts an Interface with implementing type defining isTypeOf + */ + public function testAcceptsAnInterfaceWithImplementingTypeDefiningIsTypeOf() + { + $InterfaceTypeWithoutResolveType = new InterfaceType([ + 'name' => 'InterfaceTypeWithoutResolveType', + 'fields' => ['f' => ['type' => Type::string()]] + ]); + + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'isTypeOf' => function () { + return true; + }, + 'interfaces' => [$InterfaceTypeWithoutResolveType], + 'fields' => ['f' => ['type' => Type::string()]] + ])); + + $schema->assertValid(); + } + + /** + * @it accepts an Interface type defining resolveType with implementing type defining isTypeOf + */ + public function testAcceptsAnInterfaceTypeDefiningResolveTypeWithImplementingTypeDefiningIsTypeOf() + { + $AnotherInterfaceType = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => ['f' => ['type' => Type::string()]] + ]); + + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'isTypeOf' => function () { + return true; + }, + 'interfaces' => [$AnotherInterfaceType], + 'fields' => ['f' => ['type' => Type::string()]] + ])); + + $schema->assertValid(); + } + + /** + * @it rejects an Interface type with an incorrect type for resolveType + */ + public function testRejectsAnInterfaceTypeWithAnIncorrectTypeForResolveType() + { + $type = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => new \stdClass(), + 'fields' => ['f' => ['type' => Type::string()]] + ]); + + $this->setExpectedException( + InvariantViolation::class, + 'AnotherInterface must provide "resolveType" as a function.' + ); + + $type->assertValid(); + } + + /** + * @it rejects an Interface type not defining resolveType with implementing type not defining isTypeOf + */ + public function testRejectsAnInterfaceTypeNotDefiningResolveTypeWithImplementingTypeNotDefiningIsTypeOf() + { + $InterfaceTypeWithoutResolveType = new InterfaceType([ + 'name' => 'InterfaceTypeWithoutResolveType', + 'fields' => ['f' => ['type' => Type::string()]] + ]); + + $schema = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'interfaces' => [$InterfaceTypeWithoutResolveType], + 'fields' => ['f' => ['type' => Type::string()]] + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'Interface Type InterfaceTypeWithoutResolveType does not provide a "resolveType" function and implementing '. + 'Type SomeObject does not provide a "isTypeOf" function. There is no way to resolve this implementing type '. + 'during execution.' + ); + + $schema->assertValid(); + } // 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 + + /** + * @it accepts a Union type defining resolveType + */ + public function testAcceptsAUnionTypeDefiningResolveType() + { + $schema = $this->schemaWithFieldType(new UnionType([ + 'name' => 'SomeUnion', + 'resolveType' => function () { + }, + 'types' => [$this->SomeObjectType], + ])); + $schema->assertValid(); + } + + /** + * @it accepts a Union of Object types defining isTypeOf + */ + public function testAcceptsAUnionOfObjectTypesDefiningIsTypeOf() + { + $schema = $this->schemaWithFieldType(new UnionType([ + 'name' => 'SomeUnion', + 'types' => [$this->ObjectWithIsTypeOf], + ])); + + $schema->assertValid(); + } + + /** + * @it accepts a Union type defining resolveType of Object types defining isTypeOf + */ + public function testAcceptsAUnionTypeDefiningResolveTypeOfObjectTypesDefiningIsTypeOf() + { + $schema = $this->schemaWithFieldType(new UnionType([ + 'name' => 'SomeUnion', + 'resolveType' => function () { + }, + 'types' => [$this->ObjectWithIsTypeOf], + ])); + $schema->assertValid(); + } + + /** + * @it rejects a Union type with an incorrect type for resolveType + */ + public function testRejectsAUnionTypeWithAnIncorrectTypeForResolveType() + { + $schema = $this->schemaWithFieldType(new UnionType([ + 'name' => 'SomeUnion', + 'resolveType' => new \stdClass(), + 'types' => [$this->ObjectWithIsTypeOf], + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeUnion must provide "resolveType" as a function.' + ); + + $schema->assertValid(); + } + + /** + * @it rejects a Union type not defining resolveType of Object types not defining isTypeOf + */ + public function testRejectsAUnionTypeNotDefiningResolveTypeOfObjectTypesNotDefiningIsTypeOf() + { + $schema = $this->schemaWithFieldType(new UnionType([ + 'name' => 'SomeUnion', + 'types' => [$this->SomeObjectType], + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'Union type "SomeUnion" does not provide a "resolveType" function and possible type "SomeObject" '. + 'does not provide an "isTypeOf" function. There is no way to resolve this possible type during execution.' + ); + + $schema->assertValid(); + } // 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 + + /** + * @it accepts a Scalar type defining serialize + */ + public function testAcceptsAScalarTypeDefiningSerialize() + { + $schema = $this->schemaWithFieldType(new CustomScalarType([ + 'name' => 'SomeScalar', + 'serialize' => function () { + }, + ])); + $schema->assertValid(); + } + + /** + * @it rejects a Scalar type not defining serialize + */ + public function testRejectsAScalarTypeNotDefiningSerialize() + { + $schema = $this->schemaWithFieldType(new CustomScalarType([ + 'name' => 'SomeScalar', + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeScalar must provide "serialize" function. If this custom Scalar is also used as an input type, '. + 'ensure "parseValue" and "parseLiteral" functions are also provided.' + ); + + $schema->assertValid(); + } + + /** + * @it rejects a Scalar type defining serialize with an incorrect type + */ + public function testRejectsAScalarTypeDefiningSerializeWithAnIncorrectType() + { + $schema = $this->schemaWithFieldType(new CustomScalarType([ + 'name' => 'SomeScalar', + 'serialize' => new \stdClass() + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeScalar must provide "serialize" function. If this custom Scalar ' . + 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' . + 'functions are also provided.' + ); + + $schema->assertValid(); + } + + /** + * @it accepts a Scalar type defining parseValue and parseLiteral + */ + public function testAcceptsAScalarTypeDefiningParseValueAndParseLiteral() + { + $schema = $this->schemaWithFieldType(new CustomScalarType([ + 'name' => 'SomeScalar', + 'serialize' => function () { + }, + 'parseValue' => function () { + }, + 'parseLiteral' => function () { + }, + ])); + + $schema->assertValid(); + } + + /** + * @it rejects a Scalar type defining parseValue but not parseLiteral + */ + public function testRejectsAScalarTypeDefiningParseValueButNotParseLiteral() + { + $schema = $this->schemaWithFieldType(new CustomScalarType([ + 'name' => 'SomeScalar', + 'serialize' => function () { + }, + 'parseValue' => function () { + }, + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.' + ); + $schema->assertValid(); + } + + /** + * @it rejects a Scalar type defining parseLiteral but not parseValue + */ + public function testRejectsAScalarTypeDefiningParseLiteralButNotParseValue() + { + $schema = $this->schemaWithFieldType(new CustomScalarType([ + 'name' => 'SomeScalar', + 'serialize' => function () { + }, + 'parseLiteral' => function () { + }, + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.' + ); + + $schema->assertValid(); + } + + /** + * @it rejects a Scalar type defining parseValue and parseLiteral with an incorrect type + */ + public function testRejectsAScalarTypeDefiningParseValueAndParseLiteralWithAnIncorrectType() + { + $schema = $this->schemaWithFieldType(new CustomScalarType([ + 'name' => 'SomeScalar', + 'serialize' => function () { + }, + 'parseValue' => new \stdClass(), + 'parseLiteral' => new \stdClass(), + ])); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.' + ); + + $schema->assertValid(); + } + // DESCRIBE: Type System: Enum types must be well defined - // TODO: accepts a well defined Enum type with empty value definition + + /** + * @it accepts a well defined Enum type with empty value definition + */ + public function testAcceptsAWellDefinedEnumTypeWithEmptyValueDefinition() + { + $type = new EnumType([ + 'name' => 'SomeEnum', + 'values' => [ + 'FOO' => [], + 'BAR' => [], + ] + ]); + + $type->assertValid(); + } + // 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 + + /** + * @it accepts a well defined Enum type with internal value definition + */ + public function testAcceptsAWellDefinedEnumTypeWithInternalValueDefinition() + { + $type = new EnumType([ + 'name' => 'SomeEnum', + 'values' => [ + 'FOO' => ['value' => 10], + 'BAR' => ['value' => 20], + ] + ]); + $type->assertValid(); + } + + /** + * @it rejects an Enum type without values + */ + public function testRejectsAnEnumTypeWithoutValues() + { + $type = new EnumType([ + 'name' => 'SomeEnum', + ]); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeEnum values must be an array.' + ); + + $type->assertValid(); + } + + /** + * @it rejects an Enum type with empty values + */ + public function testRejectsAnEnumTypeWithEmptyValues() + { + $type = new EnumType([ + 'name' => 'SomeEnum', + 'values' => [] + ]); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeEnum values must be not empty.' + ); + + $type->assertValid(); + } + + /** + * @it rejects an Enum type with incorrectly typed values + */ + public function testRejectsAnEnumTypeWithIncorrectlyTypedValues() + { + $type = new EnumType([ + 'name' => 'SomeEnum', + 'values' => [ + ['FOO' => 10] + ] + ]); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeEnum values must be an array with value names as keys.' + ); + $type->assertValid(); + } + + /** + * @it rejects an Enum type with missing value definition + */ + public function testRejectsAnEnumTypeWithMissingValueDefinition() + { + $type = new EnumType([ + 'name' => 'SomeEnum', + 'values' => [ + 'FOO' => null + ] + ]); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeEnum.FOO must refer to an associative array with a "value" key representing an internal value but got: null' + ); + + $type->assertValid(); + } + + /** + * @it rejects an Enum type with incorrectly typed value definition + */ + public function testRejectsAnEnumTypeWithIncorrectlyTypedValueDefinition() + { + $enumType = new EnumType([ + 'name' => 'SomeEnum', + 'values' => [ + 'FOO' => 10 + ] + ]); + + $this->setExpectedException( + InvariantViolation::class, + 'SomeEnum.FOO must refer to an associative array with a "value" key representing an internal value but got: 10' + ); + + $enumType->assertValid(); + } + + /** + * @it rejects an Enum type with incorrectly named values + */ + public function testRejectsAnEnumTypeWithIncorrectlyNamedValues() + { + $this->assertInvalidEnumValueName( + '#value', + 'SomeEnum has value with invalid name: "#value" (Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "#value" does not.)' + ); + + $this->assertInvalidEnumValueName('true', 'SomeEnum: "true" can not be used as an Enum value.'); + $this->assertInvalidEnumValueName('false', 'SomeEnum: "false" can not be used as an Enum value.'); + $this->assertInvalidEnumValueName('null', 'SomeEnum: "null" can not be used as an Enum value.'); + } + + public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnEnum() + { + $enum = new EnumType([ + 'name' => 'SomeEnum', + 'values' => [ + 'value' => ['isDeprecated' => true] + ] + ]); + $this->setExpectedException( + InvariantViolation::class, + 'SomeEnum.value should provide "deprecationReason" instead of "isDeprecated".' + ); + $enum->assertValid(); + } + + private function enumValue($name) + { + return new EnumType([ + 'name' => 'SomeEnum', + 'values' => [ + $name => [] + ] + ]); + } + + private function assertInvalidEnumValueName($name, $expectedMessage) + { + $enum = $this->enumValue($name); + + try { + $enum->assertValid(); + $this->fail('Expected exception not thrown'); + } catch (InvariantViolation $e) { + $this->assertEquals($expectedMessage, $e->getMessage()); + } + } // DESCRIBE: Type System: Object fields must have output types - // TODO: accepts an output type as an Object field type + + /** + * @it accepts an output type as an Object field type + */ + public function testAcceptsAnOutputTypeAsNnObjectFieldType() + { + foreach ($this->outputTypes as $type) { + $schema = $this->schemaWithObjectFieldOfType($type); + $schema->assertValid(); + } + } + // 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 + /** + * @it rejects an empty Object field type + */ + public function testRejectsAnEmptyObjectFieldType() + { + $schema = $this->schemaWithObjectFieldOfType(null); - // 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 + $this->setExpectedException( + InvariantViolation::class, + 'Expecting instance of GraphQL\Type\Definition\Type, got "NULL"' + ); - // 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 + $schema->assertValid(); + } - // 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 + /** + * @it rejects a non-output type as an Object field type + */ + public function testRejectsANonOutputTypeAsAnObjectFieldType() + { + foreach ($this->notOutputTypes as $type) { + $schema = $this->schemaWithObjectFieldOfType($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 + try { + $schema->assertValid(); + $this->fail('Expected exception not thrown for ' . Utils::printSafe($type)); + } catch (InvariantViolation $e) { + // FIXME + if ($type !== 'TestString') { + $this->assertEquals('BadObject.badField field type must be Output Type but got: ' . $type, $e->getMessage()); + } else { + $this->assertEquals('Expecting instance of GraphQL\Type\Definition\Type, got "string"', $e->getMessage()); + } + } + } + } // DESCRIBE: Type System: Object fields must have valid resolve values - // TODO: accepts a lambda as an Object field resolver - // TODO: rejects an empty Object field resolver - // TODO: rejects a constant scalar value resolver - // DESCRIBE: Type System: Input Object fields must not have resolvers - // TODO: accepts an Input Object type with no resolver - // TODO: accepts an Input Object type with null resolver - // TODO: accepts an Input Object type with undefined resolver - // TODO: rejects an Input Object type with resolver function - // TODO: rejects an Input Object type with resolver constant + /** + * @it accepts a lambda as an Object field resolver + */ + public function testAcceptsALambdaAsAnObjectFieldResolver() + { + $schema = $this->schemaWithObjectWithFieldResolver(function() {return [];}); + $schema->assertValid(); + } + + /** + * @it rejects an empty Object field resolver + */ + public function testRejectsAnEmptyObjectFieldResolver() + { + $schema = $this->schemaWithObjectWithFieldResolver([]); + + $this->setExpectedException( + InvariantViolation::class, + 'BadResolver.badField field resolver must be a function if provided, but got: array(0)' + ); + + $schema->assertValid(); + } + + /** + * @it rejects a constant scalar value resolver + */ + public function testRejectsAConstantScalarValueResolver() + { + $schema = $this->schemaWithObjectWithFieldResolver(0); + $this->setExpectedException( + InvariantViolation::class, + 'BadResolver.badField field resolver must be a function if provided, but got: 0' + ); + $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', + 'resolveType' => function () { + }, + '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 + + /** + * @it accepts a Union of an Object Type + */ + public function testAcceptsAUnionOfAnObjectType() + { + $schema = $this->schemaWithUnionOfType($this->SomeObjectType); + $schema->assertValid(); + } + + /** + * @it rejects a Union of a non-Object type + */ + public function testRejectsAUnionOfANonObjectType() + { + $notObjectTypes = $this->withModifiers([ + $this->SomeScalarType, + $this->SomeEnumType, + $this->SomeInterfaceType, + $this->SomeUnionType, + $this->SomeInputObjectType, + ]); + foreach ($notObjectTypes as $type) { + $schema = $this->schemaWithUnionOfType($type); + try { + $schema->assertValid(); + $this->fail('Expected exception not thrown for type: ' . $type); + } catch (InvariantViolation $e) { + $this->assertEquals( + 'BadUnion may only contain Object types, it cannot contain: ' . $type . '.', + $e->getMessage() + ); + } + } + + // "BadUnion may only contain Object types, it cannot contain: $type." + } + + + // DESCRIBE: Type System: Interface fields must have output types + + /** + * @it accepts an output type as an Interface field type + */ + public function testAcceptsAnOutputTypeAsAnInterfaceFieldType() + { + foreach ($this->outputTypes as $type) { + $schema = $this->schemaWithInterfaceFieldOfType($type); + $schema->assertValid(); + } + } + + /** + * @it rejects an empty Interface field type + */ + public function testRejectsAnEmptyInterfaceFieldType() + { + $schema = $this->schemaWithInterfaceFieldOfType(null); + + $this->setExpectedException( + InvariantViolation::class, + 'Expecting instance of GraphQL\Type\Definition\Type, got "NULL"' // FIXME + ); + + $schema->assertValid(); + } + + /** + * @it rejects a non-output type as an Interface field type + */ + public function testRejectsANonOutputTypeAsAnInterfaceFieldType() + { + foreach ($this->notOutputTypes as $type) { + $schema = $this->schemaWithInterfaceFieldOfType($type); + + try { + $schema->assertValid(); + $this->fail('Expected exception not thrown for type ' . $type); + } catch (InvariantViolation $e) { + if ($type !== 'TestString') { + $this->assertEquals( + 'BadInterface.badField field type must be Output Type but got: ' . $type, + $e->getMessage() + ); + } else { + // FIXME + $this->assertEquals( + 'Expecting instance of GraphQL\Type\Definition\Type, got "string"', + $e->getMessage() + ); + } + } + } + } + + + // DESCRIBE: Type System: Field arguments must have input types + + /** + * @it accepts an input type as a field arg type + */ + public function testAcceptsAnInputTypeAsAFieldArgType() + { + foreach ($this->inputTypes as $type) { + $schema = $this->schemaWithArgOfType($type); + $schema->assertValid(); + } + } + + /** + * @it rejects an empty field arg type + */ + public function testRejectsAnEmptyFieldArgType() + { + $schema = $this->schemaWithArgOfType(null); + + try { + $schema->assertValid(); + $this->fail('Expected exception not thrown'); + } catch (InvariantViolation $e) { + $this->assertEquals( + 'Expecting instance of GraphQL\Type\Definition\Type, got "NULL"', + $e->getMessage() + ); + } + } + + /** + * @it rejects a non-input type as a field arg type + */ + public function testRejectsANonInputTypeAsAFieldArgType() + { + foreach ($this->notInputTypes as $type) { + $schema = $this->schemaWithArgOfType($type); + try { + $schema->assertValid(); + $this->fail('Expected exception not thrown for type ' . $type); + } catch (InvariantViolation $e) { + if ($type !== 'TestString') { + $this->assertEquals( + 'BadObject.badField(badArg): argument type must be Input Type but got: ' . $type, + $e->getMessage() + ); + } else { + $this->assertEquals( + 'Expecting instance of GraphQL\Type\Definition\Type, got "string"', + $e->getMessage() + ); + } + } + } + } + + + // DESCRIBE: Type System: Input Object fields must have input types + + /** + * @it accepts an input type as an input field type + */ + public function testAcceptsAnInputTypeAsAnInputFieldType() + { + foreach ($this->inputTypes as $type) { + $schema = $this->schemaWithInputFieldOfType($type); + $schema->assertValid(); + } + } + + /** + * @it rejects an empty input field type + */ + public function testRejectsAnEmptyInputFieldType() + { + $schema = $this->schemaWithInputFieldOfType(null); + + // FIXME + $this->setExpectedException( + InvariantViolation::class, + 'Expecting instance of GraphQL\Type\Definition\Type, got "NULL"' + ); + $schema->assertValid(); + } + + /** + * @it rejects a non-input type as an input field type + */ + public function testRejectsANonInputTypeAsAnInputFieldType() + { + foreach ($this->notInputTypes as $type) { + $schema = $this->schemaWithInputFieldOfType($type); + try { + $schema->assertValid(); + $this->fail('Expected exception not thrown for type ' . $type); + } catch (InvariantViolation $e) { + if ($type !== 'TestString') { + $this->assertEquals( + "BadInputObject.badField field type must be Input Type but got: $type.", + $e->getMessage() + ); + } else { + $this->assertEquals( + 'Expecting instance of GraphQL\Type\Definition\Type, got "string"', + $e->getMessage() + ); + } + } + } + } + + + // DESCRIBE: Type System: List must accept GraphQL types + + /** + * @it accepts an type as item type of list + */ + public function testAcceptsAnTypeAsItemTypeOfList() + { + $types = $this->withModifiers([ + Type::string(), + $this->SomeScalarType, + $this->SomeObjectType, + $this->SomeUnionType, + $this->SomeInterfaceType, + $this->SomeEnumType, + $this->SomeInputObjectType, + ]); + + foreach ($types as $type) { + try { + Type::listOf($type); + } catch (\Exception $e) { + throw new \Exception("Expection thrown for type $type: {$e->getMessage()}", null, $e); + } + } + } + + /** + * @it rejects a non-type as item type of list + */ + public function testRejectsANonTypeAsItemTypeOfList() + { + $notTypes = [ + [], + new \stdClass(), + 'String', + 10, + null, + true, + false, + // TODO: function() {} + ]; + foreach ($notTypes as $type) { + try { + Type::listOf($type); + $this->fail("Expected exception not thrown for: " . Utils::printSafe($type)); + } catch (InvariantViolation $e) { + $this->assertEquals( + 'Can only create List of a GraphQLType but got: ' . Utils::printSafe($type), + $e->getMessage() + ); + } + } + } + + + // DESCRIBE: Type System: NonNull must accept GraphQL types + + /** + * @it accepts an type as nullable type of non-null + */ + public function testAcceptsAnTypeAsNullableTypeOfNonNull() + { + $nullableTypes = [ + Type::string(), + $this->SomeScalarType, + $this->SomeObjectType, + $this->SomeUnionType, + $this->SomeInterfaceType, + $this->SomeEnumType, + $this->SomeInputObjectType, + Type::listOf(Type::string()), + Type::listOf(Type::nonNull(Type::string())), + ]; + foreach ($nullableTypes as $type) { + try { + Type::nonNull($type); + } catch (\Exception $e) { + throw new \Exception("Exception thrown for type $type: " . $e->getMessage(), null, $e); + } + } + } + + // TODO: rejects a non-type as nullable type of non-null: ${type} + + /** + * @it rejects a non-type as nullable type of non-null + */ + public function testRejectsANonTypeAsNullableTypeOfNonNull() + { + $notNullableTypes = [ + Type::nonNull(Type::string()), + [], + new \stdClass(), + 'String', + null, + true, + false + ]; + foreach ($notNullableTypes as $type) { + try { + Type::nonNull($type); + $this->fail("Expected exception not thrown for: " . Utils::printSafe($type)); + } catch (InvariantViolation $e) { + $this->assertEquals( + 'Can only create NonNull of a Nullable GraphQLType but got: ' . Utils::printSafe($type), + $e->getMessage() + ); + } + } + } + + + // DESCRIBE: Objects must adhere to Interface they implement + + /** + * @it accepts an Object which implements an Interface + */ + public function testAcceptsAnObjectWhichImplementsAnInterface() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + '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()] + ] + ] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + $schema->assertValid(); + } + + /** + * @it accepts an Object which implements an Interface along with more fields + */ + public function testAcceptsAnObjectWhichImplementsAnInterfaceAlongWithMoreFields() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + '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()], + ] + ], + 'anotherfield' => ['type' => Type::string()] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + $schema->assertValid(); + } + + /** + * @it accepts an Object which implements an Interface field along with additional optional arguments + */ + public function testAcceptsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalOptionalArguments() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + '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::string()], + ] + ] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + $schema->assertValid(); + } + + /** + * @it rejects an Object which implements an Interface field along with additional required arguments + */ + public function testRejectsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalRequiredArguments() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + '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.' + ); + + $schema->assertValid(); + } + + /** + * @it rejects an Object missing an Interface field + */ + public function testRejectsAnObjectMissingAnInterfaceField() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => [ + 'field' => [ + 'type' => Type::string(), + 'args' => [ + 'input' => ['type' => Type::string()], + ] + ] + ] + ]); + + $AnotherObject = new ObjectType([ + 'name' => 'AnotherObject', + 'interfaces' => [$AnotherInterface], + 'fields' => [ + 'anotherfield' => ['type' => Type::string()] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + + $this->setExpectedException( + InvariantViolation::class, + 'AnotherInterface expects field "field" but AnotherObject does not provide it' + ); + $schema->assertValid(); + } + + /** + * @it rejects an Object with an incorrectly typed Interface field + */ + public function testRejectsAnObjectWithAnIncorrectlyTypedInterfaceField() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => [ + 'field' => ['type' => Type::string()] + ] + ]); + + $AnotherObject = new ObjectType([ + 'name' => 'AnotherObject', + 'interfaces' => [$AnotherInterface], + 'fields' => [ + 'field' => ['type' => $this->SomeScalarType] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + $this->setExpectedException( + InvariantViolation::class, + 'AnotherInterface.field expects type "String" but AnotherObject.field provides type "SomeScalar"' + ); + $schema->assertValid(); + } + + /** + * @it rejects an Object with a differently typed Interface field + */ + public function testRejectsAnObjectWithADifferentlyTypedInterfaceField() + { + $TypeA = new ObjectType([ + 'name' => 'A', + 'fields' => [ + 'foo' => ['type' => Type::string()] + ] + ]); + + $TypeB = new ObjectType([ + 'name' => 'B', + 'fields' => [ + 'foo' => ['type' => Type::string()] + ] + ]); + + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => [ + 'field' => ['type' => $TypeA] + ] + ]); + + $AnotherObject = new ObjectType([ + 'name' => 'AnotherObject', + 'interfaces' => [$AnotherInterface], + 'fields' => [ + 'field' => ['type' => $TypeB] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + + $this->setExpectedException( + InvariantViolation::class, + 'AnotherInterface.field expects type "A" but AnotherObject.field provides type "B"' + ); + + $schema->assertValid(); + } + + /** + * @it accepts an Object with a subtyped Interface field (interface) + */ + public function testAcceptsAnObjectWithASubtypedInterfaceFieldForInterface() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => function () use (&$AnotherInterface) { + return [ + 'field' => ['type' => $AnotherInterface] + ]; + } + ]); + + $AnotherObject = new ObjectType([ + 'name' => 'AnotherObject', + 'interfaces' => [$AnotherInterface], + 'fields' => function () use (&$AnotherObject) { + return [ + 'field' => ['type' => $AnotherObject] + ]; + } + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + $schema->assertValid(); + } + + /** + * @it accepts an Object with a subtyped Interface field (union) + */ + public function testAcceptsAnObjectWithASubtypedInterfaceFieldForUnion() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => [ + 'field' => ['type' => $this->SomeUnionType] + ] + ]); + + $AnotherObject = new ObjectType([ + 'name' => 'AnotherObject', + 'interfaces' => [$AnotherInterface], + 'fields' => [ + 'field' => ['type' => $this->SomeObjectType] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + $schema->assertValid(); + } + + /** + * @it rejects an Object missing an Interface argument + */ + public function testRejectsAnObjectMissingAnInterfaceArgument() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => [ + 'field' => [ + 'type' => Type::string(), + 'args' => [ + 'input' => ['type' => Type::string()], + ] + ] + ] + ]); + + $AnotherObject = new ObjectType([ + 'name' => 'AnotherObject', + 'interfaces' => [$AnotherInterface], + 'fields' => [ + 'field' => [ + 'type' => Type::string(), + ] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + + $this->setExpectedException( + InvariantViolation::class, + 'AnotherInterface.field expects argument "input" but AnotherObject.field does not provide it.' + ); + + $schema->assertValid(); + } + + /** + * @it rejects an Object with an incorrectly typed Interface argument + */ + public function testRejectsAnObjectWithAnIncorrectlyTypedInterfaceArgument() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + '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' => $this->SomeScalarType], + ] + ] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + + $this->setExpectedException( + InvariantViolation::class, + 'AnotherInterface.field(input:) expects type "String" but AnotherObject.field(input:) provides type "SomeScalar".' + ); + + $schema->assertValid(); + } + + /** + * @it accepts an Object with an equivalently modified Interface field type + */ + public function testAcceptsAnObjectWithAnEquivalentlyModifiedInterfaceFieldType() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => [ + 'field' => ['type' => Type::nonNull(Type::listOf(Type::string()))] + ] + ]); + + $AnotherObject = new ObjectType([ + 'name' => 'AnotherObject', + 'interfaces' => [$AnotherInterface], + 'fields' => [ + 'field' => ['type' => Type::nonNull(Type::listOf(Type::string()))] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + $schema->assertValid(); + } + + /** + * @it rejects an Object with a non-list Interface field list type + */ + public function testRejectsAnObjectWithANonListInterfaceFieldListType() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => [ + 'field' => ['type' => Type::listOf(Type::string())] + ] + ]); + + $AnotherObject = new ObjectType([ + 'name' => 'AnotherObject', + 'interfaces' => [$AnotherInterface], + 'fields' => [ + 'field' => ['type' => Type::string()] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + + $this->setExpectedException( + InvariantViolation::class, + 'AnotherInterface.field expects type "[String]" but AnotherObject.field provides type "String"' + ); + + $schema->assertValid(); + } + + /** + * @it rejects an Object with a list Interface field non-list type + */ + public function testRejectsAnObjectWithAListInterfaceFieldNonListType() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => [ + 'field' => ['type' => Type::string()] + ] + ]); + + $AnotherObject = new ObjectType([ + 'name' => 'AnotherObject', + 'interfaces' => [$AnotherInterface], + 'fields' => [ + 'field' => ['type' => Type::listOf(Type::string())] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + $this->setExpectedException( + InvariantViolation::class, + 'AnotherInterface.field expects type "String" but AnotherObject.field provides type "[String]"' + ); + $schema->assertValid(); + } + + /** + * @it accepts an Object with a subset non-null Interface field type + */ + public function testAcceptsAnObjectWithASubsetNonNullInterfaceFieldType() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => [ + 'field' => ['type' => Type::string()] + ] + ]); + + $AnotherObject = new ObjectType([ + 'name' => 'AnotherObject', + 'interfaces' => [$AnotherInterface], + 'fields' => [ + 'field' => ['type' => Type::nonNull(Type::string())] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + $schema->assertValid(); + } + + /** + * @it rejects an Object with a superset nullable Interface field type + */ + public function testRejectsAnObjectWithASupersetNullableInterfaceFieldType() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + }, + 'fields' => [ + 'field' => ['type' => Type::nonNull(Type::string())] + ] + ]); + + $AnotherObject = new ObjectType([ + 'name' => 'AnotherObject', + 'interfaces' => [$AnotherInterface], + 'fields' => [ + 'field' => ['type' => Type::string()] + ] + ]); + + $schema = $this->schemaWithFieldType($AnotherObject); + + $this->setExpectedException( + InvariantViolation::class, + 'AnotherInterface.field expects type "String!" but AnotherObject.field provides type "String"' + ); + + $schema->assertValid(); + } + + /** + * @it does not allow isDeprecated without deprecationReason on field + */ + public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnField() + { + $OldObject = new ObjectType([ + 'name' => 'OldObject', + 'fields' => [ + 'field' => [ + 'type' => Type::string(), + 'isDeprecated' => true + ] + ] + ]); + + $schema = $this->schemaWithFieldType($OldObject); + $this->setExpectedException( + InvariantViolation::class, + 'OldObject.field should provide "deprecationReason" instead of "isDeprecated".' + ); + $schema->assertValid(); + } private function assertEachCallableThrows($closures, $expectedError) @@ -294,9 +2788,200 @@ class ValidationTest extends \PHPUnit_Framework_TestCase return new Schema([ 'query' => new ObjectType([ 'name' => 'Query', - 'fields' => [ 'f' => $type ] + 'fields' => ['f' => ['type' => $type]] ]), - 'types' => [ $type ] + 'types' => [$type], + ]); + } + + private function schemaWithInputObject($inputObjectType) + { + return new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'f' => [ + 'type' => Type::string(), + 'args' => [ + 'input' => ['type' => $inputObjectType] + ] + ] + ] + ]) + ]); + } + + private function schemaWithObjectFieldOfType($fieldType) + { + $BadObjectType = new ObjectType([ + 'name' => 'BadObject', + 'fields' => [ + 'badField' => ['type' => $fieldType] + ] + ]); + + return new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'f' => ['type' => $BadObjectType] + ] + ]), + 'types' => [$this->SomeObjectType] + ]); + } + + private function schemaWithObjectWithFieldResolver($resolveValue) + { + $BadResolverType = new ObjectType([ + 'name' => 'BadResolver', + 'fields' => [ + 'badField' => [ + 'type' => Type::string(), + 'resolve' => $resolveValue + ] + ] + ]); + + return new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'f' => ['type' => $BadResolverType] + ] + ]) + ]); + } + + 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( + $types, + 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 schemaWithUnionOfType($type) + { + $BadUnionType = new UnionType([ + 'name' => 'BadUnion', + 'resolveType' => function () { + }, + 'types' => [$type], + ]); + return new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'f' => ['type' => $BadUnionType] + ] + ]) + ]); + } + + private function schemaWithInterfaceFieldOfType($fieldType) + { + $BadInterfaceType = new InterfaceType([ + 'name' => 'BadInterface', + 'fields' => [ + 'badField' => ['type' => $fieldType] + ] + ]); + + return new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'f' => ['type' => $BadInterfaceType] + ] + ]), + // Have to add types implementing interfaces to bypass the "could not find implementers" exception + 'types' => [ + new ObjectType([ + 'name' => 'BadInterfaceImplementer', + 'fields' => [ + 'badField' => ['type' => $fieldType] + ], + 'interfaces' => [$BadInterfaceType], + 'isTypeOf' => function() {} + ]), + $this->SomeObjectType + ] + ]); + } + + private function schemaWithArgOfType($argType) + { + $BadObjectType = new ObjectType([ + 'name' => 'BadObject', + 'fields' => [ + 'badField' => [ + 'type' => Type::string(), + 'args' => [ + 'badArg' => ['type' => $argType] + ] + ] + ] + ]); + + return new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'f' => ['type' => $BadObjectType] + ] + ]) + ]); + } + + private function schemaWithInputFieldOfType($inputFieldType) + { + $BadInputObjectType = new InputObjectType([ + 'name' => 'BadInputObject', + 'fields' => [ + 'badField' => ['type' => $inputFieldType] + ] + ]); + + return new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'f' => [ + 'type' => Type::string(), + 'args' => [ + 'badArg' => ['type' => $BadInputObjectType] + ] + ] + ] + ]) ]); } } diff --git a/tests/Utils/ExtractTypesTest.php b/tests/Utils/ExtractTypesTest.php index 4d19cc0..551d525 100644 --- a/tests/Utils/ExtractTypesTest.php +++ b/tests/Utils/ExtractTypesTest.php @@ -318,7 +318,7 @@ class ExtractTypesTest extends \PHPUnit_Framework_TestCase { $otherUserType = new ObjectType([ 'name' => 'User', - 'fields' => [] + 'fields' => ['a' => Type::string()] ]); $queryType = new ObjectType([