Support for NullValue

This commit is contained in:
vladar 2016-11-18 23:59:28 +07:00
parent 9bf8e82d7c
commit 8a676cde99
21 changed files with 408 additions and 75 deletions

View File

@ -5,17 +5,16 @@ namespace GraphQL\Executor;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\Argument; use GraphQL\Language\AST\Argument;
use GraphQL\Language\AST\NullValue;
use GraphQL\Language\AST\VariableDefinition; use GraphQL\Language\AST\VariableDefinition;
use GraphQL\Language\Printer; use GraphQL\Language\Printer;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\LeafType; use GraphQL\Type\Definition\LeafType;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Utils; use GraphQL\Utils;
@ -65,16 +64,29 @@ class Values
$valueAST = isset($argASTMap[$name]) ? $argASTMap[$name]->value : null; $valueAST = isset($argASTMap[$name]) ? $argASTMap[$name]->value : null;
$value = Utils\AST::valueFromAST($valueAST, $argDef->getType(), $variableValues); $value = Utils\AST::valueFromAST($valueAST, $argDef->getType(), $variableValues);
if (null === $value && null === $argDef->defaultValue && !$argDef->defaultValueExists()) {
continue;
}
if (null === $value) { if (null === $value) {
$value = $argDef->defaultValue; $value = $argDef->defaultValue;
} }
if (null !== $value) { if (NullValue::getNullValue() === $value) {
$result[$name] = $value; $value = null;
} }
$result[$name] = $value;
} }
return $result; return $result;
} }
/**
* @deprecated Moved to Utils\AST::valueFromAST
*
* @param $valueAST
* @param InputType $type
* @param null $variables
* @return array|null|\stdClass
*/
public static function valueFromAST($valueAST, InputType $type, $variables = null) public static function valueFromAST($valueAST, InputType $type, $variables = null)
{ {
return Utils\AST::valueFromAST($valueAST, $type, $variables); return Utils\AST::valueFromAST($valueAST, $type, $variables);
@ -105,7 +117,8 @@ class Values
if (null === $input) { if (null === $input) {
$defaultValue = $definitionAST->defaultValue; $defaultValue = $definitionAST->defaultValue;
if ($defaultValue) { if ($defaultValue) {
return Utils\AST::valueFromAST($defaultValue, $inputType); $value = Utils\AST::valueFromAST($defaultValue, $inputType);
return $value === NullValue::getNullValue() ? null : $value;
} }
} }
return self::coerceValue($inputType, $input); return self::coerceValue($inputType, $input);

View File

@ -31,6 +31,7 @@ class NodeType
const STRING = 'StringValue'; const STRING = 'StringValue';
const BOOLEAN = 'BooleanValue'; const BOOLEAN = 'BooleanValue';
const ENUM = 'EnumValue'; const ENUM = 'EnumValue';
const NULL = 'NullValue';
const LST = 'ListValue'; const LST = 'ListValue';
const OBJECT = 'ObjectValue'; const OBJECT = 'ObjectValue';
const OBJECT_FIELD = 'ObjectField'; const OBJECT_FIELD = 'ObjectField';

View File

@ -0,0 +1,13 @@
<?php
namespace GraphQL\Language\AST;
class NullValue extends Node implements Value
{
public $kind = NodeType::NULL;
public static function getNullValue()
{
static $nullValue;
return $nullValue ?: $nullValue = new \stdClass();
}
}

View File

@ -27,6 +27,7 @@ use GraphQL\Language\AST\Location;
use GraphQL\Language\AST\Name; use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\NamedType;
use GraphQL\Language\AST\NonNullType; use GraphQL\Language\AST\NonNullType;
use GraphQL\Language\AST\NullValue;
use GraphQL\Language\AST\ObjectField; use GraphQL\Language\AST\ObjectField;
use GraphQL\Language\AST\ObjectTypeDefinition; use GraphQL\Language\AST\ObjectTypeDefinition;
use GraphQL\Language\AST\ObjectValue; use GraphQL\Language\AST\ObjectValue;
@ -610,16 +611,19 @@ class Parser
* - FloatValue * - FloatValue
* - StringValue * - StringValue
* - BooleanValue * - BooleanValue
* - NullValue
* - EnumValue * - EnumValue
* - ListValue[?Const] * - ListValue[?Const]
* - ObjectValue[?Const] * - ObjectValue[?Const]
* *
* BooleanValue : one of `true` `false` * BooleanValue : one of `true` `false`
* *
* NullValue : `null`
*
* EnumValue : Name but not `true`, `false` or `null` * EnumValue : Name but not `true`, `false` or `null`
* *
* @param $isConst * @param $isConst
* @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable|ListValue|ObjectValue * @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable|ListValue|ObjectValue|NullValue
* @throws SyntaxError * @throws SyntaxError
*/ */
function parseValueLiteral($isConst) function parseValueLiteral($isConst)
@ -655,7 +659,12 @@ class Parser
'value' => $token->value === 'true', 'value' => $token->value === 'true',
'loc' => $this->loc($token) 'loc' => $this->loc($token)
]); ]);
} else if ($token->value !== 'null') { } else if ($token->value === 'null') {
$this->lexer->advance();
return new NullValue([
'loc' => $this->loc($token)
]);
} else {
$this->lexer->advance(); $this->lexer->advance();
return new EnumValue([ return new EnumValue([
'value' => $token->value, 'value' => $token->value,

View File

@ -21,11 +21,11 @@ use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\InlineFragment; use GraphQL\Language\AST\InlineFragment;
use GraphQL\Language\AST\IntValue; use GraphQL\Language\AST\IntValue;
use GraphQL\Language\AST\ListType; use GraphQL\Language\AST\ListType;
use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\NamedType;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeType; use GraphQL\Language\AST\NodeType;
use GraphQL\Language\AST\NonNullType; use GraphQL\Language\AST\NonNullType;
use GraphQL\Language\AST\NullValue;
use GraphQL\Language\AST\ObjectField; use GraphQL\Language\AST\ObjectField;
use GraphQL\Language\AST\ObjectTypeDefinition; use GraphQL\Language\AST\ObjectTypeDefinition;
use GraphQL\Language\AST\ObjectValue; use GraphQL\Language\AST\ObjectValue;
@ -112,14 +112,33 @@ class Printer
}, },
// Value // Value
NodeType::INT => function(IntValue $node) {return $node->value;}, NodeType::INT => function(IntValue $node) {
NodeType::FLOAT => function(FloatValue $node) {return $node->value;}, return $node->value;
NodeType::STRING => function(StringValue $node) {return json_encode($node->value);}, },
NodeType::BOOLEAN => function(BooleanValue $node) {return $node->value ? 'true' : 'false';}, NodeType::FLOAT => function(FloatValue $node) {
NodeType::ENUM => function(EnumValue $node) {return $node->value;}, return $node->value;
NodeType::LST => function(ListValue $node) {return '[' . $this->join($node->values, ', ') . ']';}, },
NodeType::OBJECT => function(ObjectValue $node) {return '{' . $this->join($node->fields, ', ') . '}';}, NodeType::STRING => function(StringValue $node) {
NodeType::OBJECT_FIELD => function(ObjectField $node) {return $node->name . ': ' . $node->value;}, return json_encode($node->value);
},
NodeType::BOOLEAN => function(BooleanValue $node) {
return $node->value ? 'true' : 'false';
},
NodeType::NULL => function(NullValue $node) {
return 'null';
},
NodeType::ENUM => function(EnumValue $node) {
return $node->value;
},
NodeType::LST => function(ListValue $node) {
return '[' . $this->join($node->values, ', ') . ']';
},
NodeType::OBJECT => function(ObjectValue $node) {
return '{' . $this->join($node->fields, ', ') . '}';
},
NodeType::OBJECT_FIELD => function(ObjectField $node) {
return $node->name . ': ' . $node->value;
},
// Directive // Directive
NodeType::DIRECTIVE => function(Directive $node) { NodeType::DIRECTIVE => function(Directive $node) {
@ -127,9 +146,15 @@ class Printer
}, },
// Type // Type
NodeType::NAMED_TYPE => function(NamedType $node) {return $node->name;}, NodeType::NAMED_TYPE => function(NamedType $node) {
NodeType::LIST_TYPE => function(ListType $node) {return '[' . $node->type . ']';}, return $node->name;
NodeType::NON_NULL_TYPE => function(NonNullType $node) {return $node->type . '!';}, },
NodeType::LIST_TYPE => function(ListType $node) {
return '[' . $node->type . ']';
},
NodeType::NON_NULL_TYPE => function(NonNullType $node) {
return $node->type . '!';
},
// Type System Definitions // Type System Definitions
NodeType::SCHEMA_DEFINITION => function(SchemaDefinition $def) { NodeType::SCHEMA_DEFINITION => function(SchemaDefinition $def) {
@ -139,7 +164,9 @@ class Printer
$this->block($def->operationTypes) $this->block($def->operationTypes)
], ' '); ], ' ');
}, },
NodeType::OPERATION_TYPE_DEFINITION => function(OperationTypeDefinition $def) {return $def->operation . ': ' . $def->type;}, NodeType::OPERATION_TYPE_DEFINITION => function(OperationTypeDefinition $def) {
return $def->operation . ': ' . $def->type;
},
NodeType::SCALAR_TYPE_DEFINITION => function(ScalarTypeDefinition $def) { NodeType::SCALAR_TYPE_DEFINITION => function(ScalarTypeDefinition $def) {
return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' '); return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' ');

View File

@ -65,6 +65,7 @@ class Visitor
NodeType::FLOAT => [], NodeType::FLOAT => [],
NodeType::STRING => [], NodeType::STRING => [],
NodeType::BOOLEAN => [], NodeType::BOOLEAN => [],
NodeType::NULL => [],
NodeType::ENUM => [], NodeType::ENUM => [],
NodeType::LST => ['values'], NodeType::LST => ['values'],
NodeType::OBJECT => ['fields'], NodeType::OBJECT => ['fields'],

View File

@ -40,6 +40,11 @@ class FieldArgument
*/ */
private $resolvedType; private $resolvedType;
/**
* @var bool
*/
private $defaultValueExists = false;
/** /**
* @param array $config * @param array $config
* @return array * @return array
@ -62,17 +67,23 @@ class FieldArgument
*/ */
public function __construct(array $def) public function __construct(array $def)
{ {
$def += [ foreach ($def as $key => $value) {
'type' => null, switch ($key) {
'name' => null, case 'type':
'defaultValue' => null, $this->type = $value;
'description' => null break;
]; case 'name':
$this->name = $value;
$this->type = $def['type']; break;
$this->name = $def['name']; case 'defaultValue':
$this->description = $def['description']; $this->defaultValue = $value;
$this->defaultValue = $def['defaultValue']; $this->defaultValueExists = true;
break;
case 'description':
$this->description = $value;
break;
}
}
$this->config = $def; $this->config = $def;
} }
@ -87,4 +98,12 @@ class FieldArgument
} }
return $this->resolvedType; return $this->resolvedType;
} }
/**
* @return bool
*/
public function defaultValueExists()
{
return $this->defaultValueExists;
}
} }

View File

@ -27,6 +27,13 @@ class InputObjectField
*/ */
public $type; public $type;
/**
* Helps to differentiate when `defaultValue` is `null` and when it was not even set initially
*
* @var bool
*/
private $defaultValueExists = false;
/** /**
* InputObjectField constructor. * InputObjectField constructor.
* @param array $opts * @param array $opts
@ -34,9 +41,18 @@ class InputObjectField
public function __construct(array $opts) public function __construct(array $opts)
{ {
foreach ($opts as $k => $v) { foreach ($opts as $k => $v) {
switch ($k) {
case 'defaultValue':
$this->defaultValue = $v;
$this->defaultValueExists = true;
break;
case 'defaultValueExists':
break;
default:
$this->{$k} = $v; $this->{$k} = $v;
} }
} }
}
/** /**
* @return mixed * @return mixed
@ -45,4 +61,12 @@ class InputObjectField
{ {
return Type::resolve($this->type); return Type::resolve($this->type);
} }
/**
* @return bool
*/
public function defaultValueExists()
{
return $this->defaultValueExists;
}
} }

View File

@ -8,6 +8,7 @@ use GraphQL\Language\AST\FloatValue;
use GraphQL\Language\AST\IntValue; use GraphQL\Language\AST\IntValue;
use GraphQL\Language\AST\ListValue; use GraphQL\Language\AST\ListValue;
use GraphQL\Language\AST\Name; use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\NullValue;
use GraphQL\Language\AST\ObjectField; use GraphQL\Language\AST\ObjectField;
use GraphQL\Language\AST\ObjectValue; use GraphQL\Language\AST\ObjectValue;
use GraphQL\Language\AST\StringValue; use GraphQL\Language\AST\StringValue;
@ -43,21 +44,24 @@ class AST
* | Int | Int | * | Int | Int |
* | Float | Int / Float | * | Float | Int / Float |
* | Mixed | Enum Value | * | Mixed | Enum Value |
* | null | NullValue |
* *
* @param $value * @param $value
* @param InputType $type * @param InputType $type
* @return ObjectValue|ListValue|BooleanValue|IntValue|FloatValue|EnumValue|StringValue * @return ObjectValue|ListValue|BooleanValue|IntValue|FloatValue|EnumValue|StringValue|NullValue
*/ */
static function astFromValue($value, InputType $type) static function astFromValue($value, InputType $type)
{ {
if ($type instanceof NonNull) { if ($type instanceof NonNull) {
// Note: we're not checking that the result is non-null. $astValue = self::astFromValue($value, $type->getWrappedType());
// This function is not responsible for validating the input value. if ($astValue instanceof NullValue) {
return self::astFromValue($value, $type->getWrappedType()); return null;
}
return $astValue;
} }
if ($value === null) { if ($value === null) {
return null; return new NullValue([]);
} }
// Convert PHP array to GraphQL list. If the GraphQLType is a list, but // Convert PHP array to GraphQL list. If the GraphQLType is a list, but
@ -80,7 +84,8 @@ class AST
// Populate the fields of the input object by creating ASTs from each value // Populate the fields of the input object by creating ASTs from each value
// in the PHP object according to the fields in the input type. // in the PHP object according to the fields in the input type.
if ($type instanceof InputObjectType) { if ($type instanceof InputObjectType) {
$isArrayLike = is_array($value) || $value instanceof \ArrayAccess; $isArray = is_array($value);
$isArrayLike = $isArray || $value instanceof \ArrayAccess;
if ($value === null || (!$isArrayLike && !is_object($value))) { if ($value === null || (!$isArrayLike && !is_object($value))) {
return null; return null;
} }
@ -92,15 +97,31 @@ class AST
} else { } else {
$fieldValue = isset($value->{$fieldName}) ? $value->{$fieldName} : null; $fieldValue = isset($value->{$fieldName}) ? $value->{$fieldName} : null;
} }
$fieldValue = self::astFromValue($fieldValue, $field->getType());
if ($fieldValue) { // Have to check additionally if key exists, since we differentiate between
// "no key" and "value is null":
if (null !== $fieldValue) {
$fieldExists = true;
} else if ($isArray) {
$fieldExists = array_key_exists($fieldName, $value);
} else if ($isArrayLike) {
/** @var \ArrayAccess $value */
$fieldExists = $value->offsetExists($fieldName);
} else {
$fieldExists = property_exists($value, $fieldName);
}
if ($fieldExists) {
$fieldNode = self::astFromValue($fieldValue, $field->getType());
if ($fieldNode) {
$fieldASTs[] = new ObjectField([ $fieldASTs[] = new ObjectField([
'name' => new Name(['value' => $fieldName]), 'name' => new Name(['value' => $fieldName]),
'value' => $fieldValue 'value' => $fieldNode
]); ]);
} }
} }
}
return new ObjectValue(['fields' => $fieldASTs]); return new ObjectValue(['fields' => $fieldASTs]);
} }
@ -165,11 +186,12 @@ class AST
* | String | String | * | String | String |
* | Int / Float | Int / Float | * | Int / Float | Int / Float |
* | Enum Value | Mixed | * | Enum Value | Mixed |
* | Null Value | null |
* *
* @param $valueAST * @param $valueAST
* @param InputType $type * @param InputType $type
* @param null $variables * @param null $variables
* @return array|null * @return array|null|\stdClass
* @throws \Exception * @throws \Exception
*/ */
public static function valueFromAST($valueAST, InputType $type, $variables = null) public static function valueFromAST($valueAST, InputType $type, $variables = null)
@ -182,14 +204,22 @@ class AST
} }
if (!$valueAST) { if (!$valueAST) {
return null; // When there is no AST, then there is also no value.
// Importantly, this is different from returning the GraphQL null value.
return ;
}
if ($valueAST instanceof NullValue) {
// This is explicitly returning the value null.
return NullValue::getNullValue();
} }
if ($valueAST instanceof Variable) { if ($valueAST instanceof Variable) {
$variableName = $valueAST->name->value; $variableName = $valueAST->name->value;
if (!$variables || !isset($variables[$variableName])) { if (!$variables || !isset($variables[$variableName])) {
return null; // No valid return value.
return ;
} }
// Note: we're not doing any checking that this variable is correct. We're // Note: we're not doing any checking that this variable is correct. We're
// assuming that this query has been validated and the variable usage here // assuming that this query has been validated and the variable usage here
@ -199,19 +229,23 @@ class AST
if ($type instanceof ListOfType) { if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType(); $itemType = $type->getWrappedType();
if ($valueAST instanceof ListValue) { $items = $valueAST instanceof ListValue ? $valueAST->values : [$valueAST];
return array_map(function($itemAST) use ($itemType, $variables) { $result = [];
return self::valueFromAST($itemAST, $itemType, $variables); foreach ($items as $itemAST) {
}, $valueAST->values); $value = self::valueFromAST($itemAST, $itemType, $variables);
} else { if ($value === NullValue::getNullValue()) {
return [self::valueFromAST($valueAST, $itemType, $variables)]; $value = null;
} }
$result[] = $value;
}
return $result;
} }
if ($type instanceof InputObjectType) { if ($type instanceof InputObjectType) {
$fields = $type->getFields(); $fields = $type->getFields();
if (!$valueAST instanceof ObjectValue) { if (!$valueAST instanceof ObjectValue) {
return null; // No valid return value.
return ;
} }
$fieldASTs = Utils::keyMap($valueAST->fields, function($field) {return $field->name->value;}); $fieldASTs = Utils::keyMap($valueAST->fields, function($field) {return $field->name->value;});
$values = []; $values = [];
@ -219,13 +253,20 @@ class AST
$fieldAST = isset($fieldASTs[$field->name]) ? $fieldASTs[$field->name] : null; $fieldAST = isset($fieldASTs[$field->name]) ? $fieldASTs[$field->name] : null;
$fieldValue = self::valueFromAST($fieldAST ? $fieldAST->value : null, $field->getType(), $variables); $fieldValue = self::valueFromAST($fieldAST ? $fieldAST->value : null, $field->getType(), $variables);
if (null === $fieldValue) { // If field is not in AST and defaultValue was never set for this field - do not include it in result
if (null === $fieldValue && null === $field->defaultValue && !$field->defaultValueExists()) {
continue;
}
// Set Explicit null value or default value:
if (NullValue::getNullValue() === $fieldValue) {
$fieldValue = null;
} else if (null === $fieldValue) {
$fieldValue = $field->defaultValue; $fieldValue = $field->defaultValue;
} }
if (null !== $fieldValue) {
$values[$field->name] = $fieldValue; $values[$field->name] = $fieldValue;
} }
}
return $values; return $values;
} }

View File

@ -8,6 +8,7 @@ use GraphQL\Language\AST\Document;
use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeType; use GraphQL\Language\AST\NodeType;
use GraphQL\Language\AST\NullValue;
use GraphQL\Language\AST\Value; use GraphQL\Language\AST\Value;
use GraphQL\Language\AST\Variable; use GraphQL\Language\AST\Variable;
use GraphQL\Language\Printer; use GraphQL\Language\Printer;
@ -155,7 +156,7 @@ class DocumentValidator
// A value must be provided if the type is non-null. // A value must be provided if the type is non-null.
if ($type instanceof NonNull) { if ($type instanceof NonNull) {
$wrappedType = $type->getWrappedType(); $wrappedType = $type->getWrappedType();
if (!$valueAST) { if (!$valueAST || $valueAST instanceof NullValue) {
if ($wrappedType->name) { if ($wrappedType->name) {
return [ "Expected \"{$wrappedType->name}!\", found null." ]; return [ "Expected \"{$wrappedType->name}!\", found null." ];
} }
@ -164,7 +165,7 @@ class DocumentValidator
return static::isValidLiteralValue($wrappedType, $valueAST); return static::isValidLiteralValue($wrappedType, $valueAST);
} }
if (!$valueAST) { if (!$valueAST || $valueAST instanceof NullValue) {
return []; return [];
} }

View File

@ -836,7 +836,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
$result = Executor::execute($schema, $query); $result = Executor::execute($schema, $query);
$expected = [ $expected = [
'data' => [ 'data' => [
'field' => '{"a":1,"c":0,"d":false,"e":"0","f":"some-string","h":{"a":1,"b":"test"}}' 'field' => '{"a":1,"b":null,"c":0,"d":false,"e":"0","f":"some-string","h":{"a":1,"b":"test"}}'
] ]
]; ];

View File

@ -49,6 +49,28 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
// properly parses null value to null
$doc = '
{
fieldWithObjectInput(input: {a: null, b: null, c: "C", d: null})
}
';
$ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithObjectInput' => '{"a":null,"b":null,"c":"C","d":null}']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
// properly parses null value in list
$doc = '
{
fieldWithObjectInput(input: {b: ["A",null,"C"], c: "C"})
}
';
$ast = Parser::parse($doc);
$expected = ['data' => ['fieldWithObjectInput' => '{"b":["A",null,"C"],"c":"C"}']];
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
// does not use incorrect value // does not use incorrect value
$doc = ' $doc = '
{ {

View File

@ -6,6 +6,7 @@ use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\Name; use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeType; use GraphQL\Language\AST\NodeType;
use GraphQL\Language\AST\NullValue;
use GraphQL\Language\AST\SelectionSet; use GraphQL\Language\AST\SelectionSet;
use GraphQL\Language\AST\StringValue; use GraphQL\Language\AST\StringValue;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
@ -106,15 +107,6 @@ fragment MissingOn Type
Parser::parse('{ ...on }'); Parser::parse('{ ...on }');
} }
/**
* @it does not allow null as value
*/
public function testDoesNotAllowNullAsValue()
{
$this->setExpectedException('GraphQL\Error\SyntaxError', 'Syntax Error GraphQL (1:39) Unexpected Name "null"');
Parser::parse('{ fieldWithNullableStringInput(input: null) }');
}
/** /**
* @it parses multi-byte characters * @it parses multi-byte characters
*/ */
@ -393,6 +385,17 @@ fragment $fragmentName on Type {
// Describe: parseValue // Describe: parseValue
/**
* @it parses null value
*/
public function testParsesNullValues()
{
$this->assertEquals([
'kind' => NodeType::NULL,
'loc' => ['start' => 0, 'end' => 4]
], $this->nodeToArray(Parser::parseValue('null')));
}
/** /**
* @it parses list values * @it parses list values
*/ */

View File

@ -154,7 +154,7 @@ fragment frag on Friend {
} }
{ {
unnamed(truthy: true, falsey: false) unnamed(truthy: true, falsey: false, nullish: null)
query query
} }

View File

@ -63,6 +63,7 @@ type Foo implements Bar {
four(argument: String = "string"): String four(argument: String = "string"): String
five(argument: [String] = ["string", "string"]): String five(argument: [String] = ["string", "string"]): String
six(argument: InputType = {key: "value"}): Type six(argument: InputType = {key: "value"}): Type
seven(argument: Int = null): Type
} }
type AnnotatedObject @onObject(arg: "value") { type AnnotatedObject @onObject(arg: "value") {

View File

@ -636,6 +636,12 @@ class VisitorTest extends \PHPUnit_Framework_TestCase
[ 'enter', 'BooleanValue', 'value', 'Argument' ], [ 'enter', 'BooleanValue', 'value', 'Argument' ],
[ 'leave', 'BooleanValue', 'value', 'Argument' ], [ 'leave', 'BooleanValue', 'value', 'Argument' ],
[ 'leave', 'Argument', 1, null ], [ 'leave', 'Argument', 1, null ],
[ 'enter', 'Argument', 2, null ],
[ 'enter', 'Name', 'name', 'Argument' ],
[ 'leave', 'Name', 'name', 'Argument' ],
[ 'enter', 'NullValue', 'value', 'Argument' ],
[ 'leave', 'NullValue', 'value', 'Argument' ],
[ 'leave', 'Argument', 2, null ],
[ 'leave', 'Field', 0, null ], [ 'leave', 'Field', 0, null ],
[ 'enter', 'Field', 1, null ], [ 'enter', 'Field', 1, null ],
[ 'enter', 'Name', 'name', 'Field' ], [ 'enter', 'Name', 'name', 'Field' ],

View File

@ -52,6 +52,6 @@ fragment frag on Friend {
} }
{ {
unnamed(truthy: true, falsey: false), unnamed(truthy: true, falsey: false, nullish: null),
query query
} }

View File

@ -17,6 +17,7 @@ type Foo implements Bar {
four(argument: String = "string"): String four(argument: String = "string"): String
five(argument: [String] = ["string", "string"]): String five(argument: [String] = ["string", "string"]): String
six(argument: InputType = {key: "value"}): Type six(argument: InputType = {key: "value"}): Type
seven(argument: Int = null): Type
} }
type AnnotatedObject @onObject(arg: "value") { type AnnotatedObject @onObject(arg: "value") {

View File

@ -7,6 +7,7 @@ use GraphQL\Language\AST\FloatValue;
use GraphQL\Language\AST\IntValue; use GraphQL\Language\AST\IntValue;
use GraphQL\Language\AST\ListValue; use GraphQL\Language\AST\ListValue;
use GraphQL\Language\AST\Name; use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\NullValue;
use GraphQL\Language\AST\ObjectField; use GraphQL\Language\AST\ObjectField;
use GraphQL\Language\AST\ObjectValue; use GraphQL\Language\AST\ObjectValue;
use GraphQL\Language\AST\StringValue; use GraphQL\Language\AST\StringValue;
@ -26,9 +27,11 @@ class ASTFromValueTest extends \PHPUnit_Framework_TestCase
{ {
$this->assertEquals(new BooleanValue(['value' => true]), AST::astFromValue(true, Type::boolean())); $this->assertEquals(new BooleanValue(['value' => true]), AST::astFromValue(true, Type::boolean()));
$this->assertEquals(new BooleanValue(['value' => false]), AST::astFromValue(false, Type::boolean())); $this->assertEquals(new BooleanValue(['value' => false]), AST::astFromValue(false, Type::boolean()));
$this->assertEquals(null, AST::astFromValue(null, Type::boolean())); $this->assertEquals(new NullValue([]), AST::astFromValue(null, Type::boolean()));
$this->assertEquals(new BooleanValue(['value' => false]), AST::astFromValue(0, Type::boolean())); $this->assertEquals(new BooleanValue(['value' => false]), AST::astFromValue(0, Type::boolean()));
$this->assertEquals(new BooleanValue(['value' => true]), AST::astFromValue(1, Type::boolean())); $this->assertEquals(new BooleanValue(['value' => true]), AST::astFromValue(1, Type::boolean()));
$this->assertEquals(new BooleanValue(['value' => false]), AST::astFromValue(0, Type::nonNull(Type::boolean())));
$this->assertEquals(null, AST::astFromValue(null, Type::nonNull(Type::boolean()))); // Note: null means that AST cannot
} }
/** /**
@ -70,7 +73,8 @@ class ASTFromValueTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(new StringValue(['value' => 'VA\\nLUE']), AST::astFromValue("VA\nLUE", Type::string())); $this->assertEquals(new StringValue(['value' => 'VA\\nLUE']), AST::astFromValue("VA\nLUE", Type::string()));
$this->assertEquals(new StringValue(['value' => '123']), AST::astFromValue(123, Type::string())); $this->assertEquals(new StringValue(['value' => '123']), AST::astFromValue(123, Type::string()));
$this->assertEquals(new StringValue(['value' => 'false']), AST::astFromValue(false, Type::string())); $this->assertEquals(new StringValue(['value' => 'false']), AST::astFromValue(false, Type::string()));
$this->assertEquals(null, AST::astFromValue(null, Type::string())); $this->assertEquals(new NullValue([]), AST::astFromValue(null, Type::string()));
$this->assertEquals(null, AST::astFromValue(null, Type::nonNull(Type::string())));
} }
/** /**
@ -83,7 +87,16 @@ class ASTFromValueTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(new StringValue(['value' => 'VA\\nLUE']), AST::astFromValue("VA\nLUE", Type::id())); $this->assertEquals(new StringValue(['value' => 'VA\\nLUE']), AST::astFromValue("VA\nLUE", Type::id()));
$this->assertEquals(new IntValue(['value' => '123']), AST::astFromValue(123, Type::id())); $this->assertEquals(new IntValue(['value' => '123']), AST::astFromValue(123, Type::id()));
$this->assertEquals(new StringValue(['value' => 'false']), AST::astFromValue(false, Type::id())); $this->assertEquals(new StringValue(['value' => 'false']), AST::astFromValue(false, Type::id()));
$this->assertEquals(null, AST::astFromValue(null, Type::id())); $this->assertEquals(new NullValue([]), AST::astFromValue(null, Type::id()));
$this->assertEquals(null, AST::astFromValue(null, Type::nonNull(Type::id())));
}
/**
* @it does not converts NonNull values to NullValue
*/
public function testDoesNotConvertsNonNullValuestoNullValue()
{
$this->assertSame(null, AST::astFromValue(null, Type::nonNull(Type::boolean())));
} }
/** /**
@ -156,6 +169,44 @@ class ASTFromValueTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, AST::astFromValue((object) $data, $inputObj)); $this->assertEquals($expected, AST::astFromValue((object) $data, $inputObj));
} }
public function testConvertsInputObjectsWithExplicitNulls()
{
$inputObj = new InputObjectType([
'name' => 'MyInputObj',
'fields' => [
'foo' => Type::float(),
'bar' => $this->myEnum()
]
]);
$this->assertEquals(new ObjectValue([
'fields' => [
$this->objectField('foo', new NullValue([]))
]
]), AST::astFromValue(['foo' => null], $inputObj));
/*
const inputObj = new GraphQLInputObjectType({
name: 'MyInputObj',
fields: {
foo: { type: GraphQLFloat },
bar: { type: myEnum },
}
});
expect(astFromValue(
{ foo: null },
inputObj
)).to.deep.equal(
{ kind: 'ObjectValue',
fields: [
{ kind: 'ObjectField',
name: { kind: 'Name', value: 'foo' },
value: { kind: 'NullValue' } } ] }
);
});
*/
}
private $complexValue; private $complexValue;
private function complexValue() private function complexValue()

View File

@ -132,6 +132,28 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it null into nullable type
*/
public function testNullIntoNullableType()
{
$this->expectPassesRule(new ArgumentsOfCorrectType(), '
{
complicatedArgs {
intArgField(intArg: null)
}
}
');
$this->expectPassesRule(new ArgumentsOfCorrectType(), '
{
dog(a: null, b: null, c:{ requiredField: true, intField: null }) {
name
}
}
');
}
// Invalid String values // Invalid String values
/** /**
@ -574,6 +596,20 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Null value
*/
public function testNullValue()
{
$this->expectPassesRule(new ArgumentsOfCorrectType(), '
{
complicatedArgs {
stringListArgField(stringListArg: null)
}
}
');
}
/** /**
* @it Single value into List * @it Single value into List
*/ */
@ -801,6 +837,24 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Null value
*/
public function testNullValue2()
{
$this->expectFailsRule(new ArgumentsOfCorrectType(), '
{
complicatedArgs {
multipleReqs(req1: null)
}
}
', [
$this->badValue('req1', 'Int!', 'null', 4, 32, [
'Expected "Int!", found null.'
]),
]);
}
// Valid input object value // Valid input object value

View File

@ -50,6 +50,52 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it variables with valid default null values
*/
public function testVariablesWithValidDefaultNullValues()
{
$this->expectPassesRule(new DefaultValuesOfCorrectType(), '
query WithDefaultValues(
$a: Int = null,
$b: String = null,
$c: ComplexInput = { requiredField: true, intField: null }
) {
dog { name }
}
');
}
/**
* @it variables with invalid default null values
*/
public function testVariablesWithInvalidDefaultNullValues()
{
$this->expectFailsRule(new DefaultValuesOfCorrectType(), '
query WithDefaultValues(
$a: Int! = null,
$b: String! = null,
$c: ComplexInput = { requiredField: null, intField: null }
) {
dog { name }
}
', [
$this->defaultForNonNullArg('a', 'Int!', 'Int', 3, 20),
$this->badValue('a', 'Int!', 'null', 3, 20, [
'Expected "Int!", found null.'
]),
$this->defaultForNonNullArg('b', 'String!', 'String', 4, 23),
$this->badValue('b', 'String!', 'null', 4, 23, [
'Expected "String!", found null.'
]),
$this->badValue('c', 'ComplexInput', '{requiredField: null, intField: null}',
5, 28, [
'In field "requiredField": Expected "Boolean!", found null.'
]
),
]);
}
/** /**
* @it no required variables with default values * @it no required variables with default values
*/ */