Better Predicates

Introduces new assertion functions for each kind of type mirroring the existing ones for the higher order types.

ref: graphql/graphql-js#1137
This commit is contained in:
Daniel Tschinder 2018-02-13 18:04:03 +01:00
parent 60df83f47e
commit 9387548aa1
13 changed files with 181 additions and 134 deletions

View File

@ -2115,7 +2115,7 @@ static function valueFromASTUntyped($valueNode, array $variables = null)
* @param Schema $schema * @param Schema $schema
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
* @return Type * @return Type
* @throws InvariantViolation * @throws \Exception
*/ */
static function typeFromAST(GraphQL\Type\Schema $schema, $inputTypeNode) static function typeFromAST(GraphQL\Type\Schema $schema, $inputTypeNode)
``` ```

View File

@ -12,6 +12,20 @@ use GraphQL\Utils\Utils;
*/ */
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType class InterfaceType extends Type implements AbstractType, OutputType, CompositeType
{ {
/**
* @param mixed $type
* @return self
*/
public static function assertInterfaceType($type)
{
Utils::invariant(
$type instanceof self,
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Interface type.'
);
return $type;
}
/** /**
* @var FieldDefinition[] * @var FieldDefinition[]
*/ */

View File

@ -20,12 +20,7 @@ class ListOfType extends Type implements WrappingType, OutputType, InputType
*/ */
public function __construct($type) public function __construct($type)
{ {
if (!$type instanceof Type && !is_callable($type)) { $this->ofType = Type::assertType($type);
throw new InvariantViolation(
'Can only create List of a GraphQLType but got: ' . Utils::printSafe($type)
);
}
$this->ofType = $type;
} }
/** /**

View File

@ -11,7 +11,35 @@ use GraphQL\Utils\Utils;
class NonNull extends Type implements WrappingType, OutputType, InputType class NonNull extends Type implements WrappingType, OutputType, InputType
{ {
/** /**
* @var ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType * @param mixed $type
* @return self
*/
public static function assertNullType($type)
{
Utils::invariant(
$type instanceof self,
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Non-Null type.'
);
return $type;
}
/**
* @param mixed $type
* @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType
*/
public static function assertNullableType($type)
{
Utils::invariant(
Type::isType($type) && !$type instanceof self,
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL nullable type.'
);
return $type;
}
/**
* @var ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType
*/ */
private $ofType; private $ofType;
@ -21,37 +49,17 @@ class NonNull extends Type implements WrappingType, OutputType, InputType
*/ */
public function __construct($type) public function __construct($type)
{ {
if (!$type instanceof Type && !is_callable($type)) { $this->ofType = self::assertNullableType($type);
throw new InvariantViolation(
'Can only create NonNull of a Nullable GraphQLType but got: ' . Utils::printSafe($type)
);
}
if ($type instanceof NonNull) {
throw new InvariantViolation(
'Can only create NonNull of a Nullable GraphQLType but got: ' . Utils::printSafe($type)
);
}
Utils::invariant(
!($type instanceof NonNull),
'Cannot nest NonNull inside NonNull'
);
$this->ofType = $type;
} }
/** /**
* @param bool $recurse * @param bool $recurse
* @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType
* @throws InvariantViolation * @throws InvariantViolation
*/ */
public function getWrappedType($recurse = false) public function getWrappedType($recurse = false)
{ {
$type = $this->ofType; $type = $this->ofType;
Utils::invariant(
!($type instanceof NonNull),
'Cannot nest NonNull inside NonNull'
);
return ($recurse && $type instanceof WrappingType) ? $type->getWrappedType($recurse) : $type; return ($recurse && $type instanceof WrappingType) ? $type->getWrappedType($recurse) : $type;
} }

View File

@ -49,6 +49,20 @@ use GraphQL\Utils\Utils;
*/ */
class ObjectType extends Type implements OutputType, CompositeType class ObjectType extends Type implements OutputType, CompositeType
{ {
/**
* @param mixed $type
* @return self
*/
public static function assertObjectType($type)
{
Utils::invariant(
$type instanceof self,
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Object type.'
);
return $type;
}
/** /**
* @var FieldDefinition[] * @var FieldDefinition[]
*/ */

View File

@ -4,8 +4,10 @@ namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\ListType; use GraphQL\Language\AST\ListType;
use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\NamedType;
use GraphQL\Language\AST\NonNullType;
use GraphQL\Language\AST\TypeDefinitionNode; use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Type\Introspection; use GraphQL\Type\Introspection;
use GraphQL\Utils\Utils;
/** /**
* Registry of standard GraphQL types * Registry of standard GraphQL types
@ -218,11 +220,25 @@ abstract class Type implements \JsonSerializable
$type instanceof UnionType || $type instanceof UnionType ||
$type instanceof EnumType || $type instanceof EnumType ||
$type instanceof InputObjectType || $type instanceof InputObjectType ||
$type instanceof ListType || $type instanceof ListOfType ||
$type instanceof NonNull $type instanceof NonNull
); );
} }
/**
* @param mixed $type
* @return mixed
*/
public static function assertType($type)
{
Utils::invariant(
self::isType($type),
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL type.'
);
return $type;
}
/** /**
* @api * @api
* @param Type $type * @param Type $type

View File

@ -1,6 +1,7 @@
<?php <?php
namespace GraphQL\Utils; namespace GraphQL\Utils;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\DocumentNode;
@ -205,51 +206,50 @@ class AST
return new ObjectValueNode(['fields' => $fieldNodes]); return new ObjectValueNode(['fields' => $fieldNodes]);
} }
Utils::invariant( if ($type instanceof ScalarType || $type instanceof EnumType) {
$type instanceof ScalarType || $type instanceof EnumType, // Since value is an internally represented value, it must be serialized
"Must provide Input Type, cannot use: " . Utils::printSafe($type) // to an externally represented value before converting into an AST.
); $serialized = $type->serialize($value);
if (null === $serialized || Utils::isInvalid($serialized)) {
return null;
}
// Since value is an internally represented value, it must be serialized // Others serialize based on their corresponding PHP scalar types.
// to an externally represented value before converting into an AST. if (is_bool($serialized)) {
$serialized = $type->serialize($value); return new BooleanValueNode(['value' => $serialized]);
if (null === $serialized || Utils::isInvalid($serialized)) { }
return null; if (is_int($serialized)) {
}
// Others serialize based on their corresponding PHP scalar types.
if (is_bool($serialized)) {
return new BooleanValueNode(['value' => $serialized]);
}
if (is_int($serialized)) {
return new IntValueNode(['value' => $serialized]);
}
if (is_float($serialized)) {
if ((int) $serialized == $serialized) {
return new IntValueNode(['value' => $serialized]); return new IntValueNode(['value' => $serialized]);
} }
return new FloatValueNode(['value' => $serialized]); if (is_float($serialized)) {
} if ((int) $serialized == $serialized) {
if (is_string($serialized)) { return new IntValueNode(['value' => $serialized]);
// Enum types use Enum literals. }
if ($type instanceof EnumType) { return new FloatValueNode(['value' => $serialized]);
return new EnumValueNode(['value' => $serialized]); }
if (is_string($serialized)) {
// Enum types use Enum literals.
if ($type instanceof EnumType) {
return new EnumValueNode(['value' => $serialized]);
}
// ID types can use Int literals.
$asInt = (int) $serialized;
if ($type instanceof IDType && (string) $asInt === $serialized) {
return new IntValueNode(['value' => $serialized]);
}
// Use json_encode, which uses the same string encoding as GraphQL,
// then remove the quotes.
return new StringValueNode([
'value' => substr(json_encode($serialized), 1, -1)
]);
} }
// ID types can use Int literals. throw new InvariantViolation('Cannot convert value to AST: ' . Utils::printSafe($serialized));
$asInt = (int) $serialized;
if ($type instanceof IDType && (string) $asInt === $serialized) {
return new IntValueNode(['value' => $serialized]);
}
// Use json_encode, which uses the same string encoding as GraphQL,
// then remove the quotes.
return new StringValueNode([
'value' => substr(json_encode($serialized), 1, -1)
]);
} }
throw new InvariantViolation('Cannot convert value to AST: ' . Utils::printSafe($serialized)); throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
} }
/** /**
@ -395,25 +395,26 @@ class AST
return $enumValue->value; return $enumValue->value;
} }
Utils::invariant($type instanceof ScalarType, 'Must be scalar type'); if ($type instanceof ScalarType) {
/** @var ScalarType $type */ // Scalars fulfill parsing a literal value via parseLiteral().
// Invalid values represent a failure to parse correctly, in which case
// no value is returned.
try {
$result = $type->parseLiteral($valueNode, $variables);
} catch (\Exception $error) {
return $undefined;
} catch (\Throwable $error) {
return $undefined;
}
// Scalars fulfill parsing a literal value via parseLiteral(). if (Utils::isInvalid($result)) {
// Invalid values represent a failure to parse correctly, in which case return $undefined;
// no value is returned. }
try {
$result = $type->parseLiteral($valueNode, $variables); return $result;
} catch (\Exception $error) {
return $undefined;
} catch (\Throwable $error) {
return $undefined;
} }
if (Utils::isInvalid($result)) { throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
return $undefined;
}
return $result;
} }
/** /**
@ -473,9 +474,9 @@ class AST
return ($variables && isset($variables[$variableName]) && !Utils::isInvalid($variables[$variableName])) return ($variables && isset($variables[$variableName]) && !Utils::isInvalid($variables[$variableName]))
? $variables[$variableName] ? $variables[$variableName]
: null; : null;
default: }
throw new InvariantViolation('Unexpected value kind: ' . $valueNode->kind);
} throw new Error('Unexpected value kind: ' . $valueNode->kind . '.');
} }
/** /**
@ -485,7 +486,7 @@ class AST
* @param Schema $schema * @param Schema $schema
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
* @return Type * @return Type
* @throws InvariantViolation * @throws \Exception
*/ */
public static function typeFromAST(Schema $schema, $inputTypeNode) public static function typeFromAST(Schema $schema, $inputTypeNode)
{ {
@ -497,9 +498,11 @@ class AST
$innerType = self::typeFromAST($schema, $inputTypeNode->type); $innerType = self::typeFromAST($schema, $inputTypeNode->type);
return $innerType ? new NonNull($innerType) : null; return $innerType ? new NonNull($innerType) : null;
} }
if ($inputTypeNode instanceof NamedTypeNode) {
return $schema->getType($inputTypeNode->name->value);
}
Utils::invariant($inputTypeNode && $inputTypeNode instanceof NamedTypeNode, 'Must be a named type'); throw new Error('Unexpected type kind: ' . $inputTypeNode->kind . '.');
return $schema->getType($inputTypeNode->name->value);
} }
/** /**

View File

@ -75,8 +75,7 @@ class ASTDefinitionBuilder
} }
if ($inputTypeNode->kind == NodeKind::NON_NULL_TYPE) { if ($inputTypeNode->kind == NodeKind::NON_NULL_TYPE) {
$wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type); $wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type);
Utils::invariant(!($wrappedType instanceof NonNull), 'No nesting nonnull.'); return Type::nonNull(NonNull::assertNullableType($wrappedType));
return Type::nonNull($wrappedType);
} }
return $innerType; return $innerType;
} }
@ -159,8 +158,7 @@ class ASTDefinitionBuilder
public function buildObjectType($typeNode) public function buildObjectType($typeNode)
{ {
$type = $this->buildType($typeNode); $type = $this->buildType($typeNode);
Utils::invariant($type instanceof ObjectType, 'Expected Object type.' . get_class($type)); return ObjectType::assertObjectType($type);
return $type;
} }
/** /**
@ -171,8 +169,7 @@ class ASTDefinitionBuilder
public function buildInterfaceType($typeNode) public function buildInterfaceType($typeNode)
{ {
$type = $this->buildType($typeNode); $type = $this->buildType($typeNode);
Utils::invariant($type instanceof InterfaceType, 'Expected Interface type.'); return InterfaceType::assertInterfaceType($type);
return $type;
} }
/** /**

View File

@ -148,8 +148,11 @@ class FindBreakingChanges
$dangerousChanges = []; $dangerousChanges = [];
foreach ($oldTypeMap as $oldTypeName => $oldTypeDefinition) { foreach ($oldTypeMap as $oldTypeName => $oldTypeDefinition) {
$newTypeDefinition = isset($newTypeMap[$oldTypeName]) ? $newTypeMap[$oldTypeName] : null; $newTypeDefinition = isset($newTypeMap[$oldTypeName]) ? $newTypeMap[$oldTypeName] : null;
if (!($oldTypeDefinition instanceof ObjectType || $oldTypeDefinition instanceof InterfaceType) || if (
!($newTypeDefinition instanceof $oldTypeDefinition)) { !($oldTypeDefinition instanceof ObjectType || $oldTypeDefinition instanceof InterfaceType) ||
!($newTypeDefinition instanceof ObjectType || $newTypeDefinition instanceof InterfaceType) ||
!($newTypeDefinition instanceof $oldTypeDefinition)
) {
continue; continue;
} }
@ -262,7 +265,11 @@ class FindBreakingChanges
$breakingChanges = []; $breakingChanges = [];
foreach ($oldTypeMap as $typeName => $oldType) { foreach ($oldTypeMap as $typeName => $oldType) {
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
if (!($oldType instanceof ObjectType || $oldType instanceof InterfaceType) || !($newType instanceof $oldType)) { if (
!($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
!($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
!($newType instanceof $oldType)
) {
continue; continue;
} }
$oldTypeFieldsDef = $oldType->getFields(); $oldTypeFieldsDef = $oldType->getFields();

View File

@ -1,6 +1,7 @@
<?php <?php
namespace GraphQL\Utils; namespace GraphQL\Utils;
use GraphQL\Error\Error;
use GraphQL\Language\Printer; use GraphQL\Language\Printer;
use GraphQL\Type\Introspection; use GraphQL\Type\Introspection;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
@ -142,9 +143,11 @@ class SchemaPrinter
return self::printUnion($type, $options); return self::printUnion($type, $options);
} else if ($type instanceof EnumType) { } else if ($type instanceof EnumType) {
return self::printEnum($type, $options); return self::printEnum($type, $options);
} else if ($type instanceof InputObjectType) {
return self::printInputObject($type, $options);
} }
Utils::invariant($type instanceof InputObjectType);
return self::printInputObject($type, $options); throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
} }
private static function printScalar(ScalarType $type, array $options) private static function printScalar(ScalarType $type, array $options)

View File

@ -319,27 +319,28 @@ class DocumentValidator
return []; return [];
} }
Utils::invariant($type instanceof ScalarType, 'Must be a scalar type'); if ($type instanceof ScalarType) {
/** @var ScalarType $type */ // Scalars determine if a literal values is valid via parseLiteral().
try {
// Scalars determine if a literal values is valid via parseLiteral(). $parseResult = $type->parseLiteral($valueNode);
try { if (Utils::isInvalid($parseResult)) {
$parseResult = $type->parseLiteral($valueNode); $printed = Printer::doPrint($valueNode);
if (Utils::isInvalid($parseResult)) { return ["Expected type \"{$type->name}\", found $printed."];
}
} catch (\Exception $error) {
$printed = Printer::doPrint($valueNode); $printed = Printer::doPrint($valueNode);
return ["Expected type \"{$type->name}\", found $printed."]; $message = $error->getMessage();
return ["Expected type \"{$type->name}\", found $printed; $message"];
} catch (\Throwable $error) {
$printed = Printer::doPrint($valueNode);
$message = $error->getMessage();
return ["Expected type \"{$type->name}\", found $printed; $message"];
} }
} catch (\Exception $error) {
$printed = Printer::doPrint($valueNode); return [];
$message = $error->getMessage();
return ["Expected type \"{$type->name}\", found $printed; $message"];
} catch (\Throwable $error) {
$printed = Printer::doPrint($valueNode);
$message = $error->getMessage();
return ["Expected type \"{$type->name}\", found $printed; $message"];
} }
return []; throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
} }
/** /**

View File

@ -468,15 +468,6 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
} }
} }
/**
* @it prohibits nesting NonNull inside NonNull
*/
public function testProhibitsNonNullNesting()
{
$this->setExpectedException('\Exception');
new NonNull(new NonNull(Type::int()));
}
/** /**
* @it prohibits putting non-Object types in unions * @it prohibits putting non-Object types in unions
*/ */

View File

@ -1975,7 +1975,7 @@ class ValidationTest extends \PHPUnit_Framework_TestCase
} }
// DESCRIBE: Type System: List must accept GraphQL types // DESCRIBE: Type System: List must accept only types
/** /**
* @it accepts an type as item type of list * @it accepts an type as item type of list
@ -2022,7 +2022,7 @@ class ValidationTest extends \PHPUnit_Framework_TestCase
$this->fail("Expected exception not thrown for: " . Utils::printSafe($type)); $this->fail("Expected exception not thrown for: " . Utils::printSafe($type));
} catch (InvariantViolation $e) { } catch (InvariantViolation $e) {
$this->assertEquals( $this->assertEquals(
'Can only create List of a GraphQLType but got: ' . Utils::printSafe($type), 'Expected '. Utils::printSafe($type) . ' to be a GraphQL type.',
$e->getMessage() $e->getMessage()
); );
} }
@ -2030,7 +2030,7 @@ class ValidationTest extends \PHPUnit_Framework_TestCase
} }
// DESCRIBE: Type System: NonNull must accept GraphQL types // DESCRIBE: Type System: NonNull must only accept non-nullable types
/** /**
* @it accepts an type as nullable type of non-null * @it accepts an type as nullable type of non-null
@ -2057,8 +2057,6 @@ class ValidationTest extends \PHPUnit_Framework_TestCase
} }
} }
// TODO: rejects a non-type as nullable type of non-null: ${type}
/** /**
* @it rejects a non-type as nullable type of non-null * @it rejects a non-type as nullable type of non-null
*/ */
@ -2079,7 +2077,7 @@ class ValidationTest extends \PHPUnit_Framework_TestCase
$this->fail("Expected exception not thrown for: " . Utils::printSafe($type)); $this->fail("Expected exception not thrown for: " . Utils::printSafe($type));
} catch (InvariantViolation $e) { } catch (InvariantViolation $e) {
$this->assertEquals( $this->assertEquals(
'Can only create NonNull of a Nullable GraphQLType but got: ' . Utils::printSafe($type), 'Expected ' . Utils::printSafe($type) . ' to be a GraphQL nullable type.',
$e->getMessage() $e->getMessage()
); );
} }