From 8a676cde99903b10e5f3cec719be488abe0585b1 Mon Sep 17 00:00:00 2001 From: vladar Date: Fri, 18 Nov 2016 23:59:28 +0700 Subject: [PATCH] Support for NullValue --- src/Executor/Values.php | 23 ++++- src/Language/AST/NodeType.php | 1 + src/Language/AST/NullValue.php | 13 +++ src/Language/Parser.php | 13 ++- src/Language/Printer.php | 53 ++++++++--- src/Language/Visitor.php | 1 + src/Type/Definition/FieldArgument.php | 41 +++++--- src/Type/Definition/InputObjectField.php | 26 +++++- src/Utils/AST.php | 93 +++++++++++++------ src/Validator/DocumentValidator.php | 5 +- tests/Executor/ExecutorTest.php | 2 +- tests/Executor/VariablesTest.php | 22 +++++ tests/Language/ParserTest.php | 21 +++-- tests/Language/PrinterTest.php | 2 +- tests/Language/SchemaPrinterTest.php | 1 + tests/Language/VisitorTest.php | 6 ++ tests/Language/kitchen-sink.graphql | 2 +- tests/Language/schema-kitchen-sink.graphql | 1 + tests/Utils/AstFromValueTest.php | 57 +++++++++++- .../Validator/ArgumentsOfCorrectTypeTest.php | 54 +++++++++++ .../DefaultValuesOfCorrectTypeTest.php | 46 +++++++++ 21 files changed, 408 insertions(+), 75 deletions(-) create mode 100644 src/Language/AST/NullValue.php diff --git a/src/Executor/Values.php b/src/Executor/Values.php index 0efe9b2..18bba17 100644 --- a/src/Executor/Values.php +++ b/src/Executor/Values.php @@ -5,17 +5,16 @@ namespace GraphQL\Executor; use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\Argument; +use GraphQL\Language\AST\NullValue; use GraphQL\Language\AST\VariableDefinition; use GraphQL\Language\Printer; use GraphQL\Schema; -use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\LeafType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; -use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Utils; @@ -65,16 +64,29 @@ class Values $valueAST = isset($argASTMap[$name]) ? $argASTMap[$name]->value : null; $value = Utils\AST::valueFromAST($valueAST, $argDef->getType(), $variableValues); + if (null === $value && null === $argDef->defaultValue && !$argDef->defaultValueExists()) { + continue; + } + if (null === $value) { $value = $argDef->defaultValue; } - if (null !== $value) { - $result[$name] = $value; + if (NullValue::getNullValue() === $value) { + $value = null; } + $result[$name] = $value; } 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) { return Utils\AST::valueFromAST($valueAST, $type, $variables); @@ -105,7 +117,8 @@ class Values if (null === $input) { $defaultValue = $definitionAST->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); diff --git a/src/Language/AST/NodeType.php b/src/Language/AST/NodeType.php index 4b73d8e..42886a7 100644 --- a/src/Language/AST/NodeType.php +++ b/src/Language/AST/NodeType.php @@ -31,6 +31,7 @@ class NodeType const STRING = 'StringValue'; const BOOLEAN = 'BooleanValue'; const ENUM = 'EnumValue'; + const NULL = 'NullValue'; const LST = 'ListValue'; const OBJECT = 'ObjectValue'; const OBJECT_FIELD = 'ObjectField'; diff --git a/src/Language/AST/NullValue.php b/src/Language/AST/NullValue.php new file mode 100644 index 0000000..e700bed --- /dev/null +++ b/src/Language/AST/NullValue.php @@ -0,0 +1,13 @@ + $token->value === 'true', '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(); return new EnumValue([ 'value' => $token->value, diff --git a/src/Language/Printer.php b/src/Language/Printer.php index b42d88b..1595ae3 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -21,11 +21,11 @@ use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\InlineFragment; use GraphQL\Language\AST\IntValue; use GraphQL\Language\AST\ListType; -use GraphQL\Language\AST\Name; use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeType; use GraphQL\Language\AST\NonNullType; +use GraphQL\Language\AST\NullValue; use GraphQL\Language\AST\ObjectField; use GraphQL\Language\AST\ObjectTypeDefinition; use GraphQL\Language\AST\ObjectValue; @@ -112,14 +112,33 @@ class Printer }, // Value - NodeType::INT => function(IntValue $node) {return $node->value;}, - NodeType::FLOAT => function(FloatValue $node) {return $node->value;}, - NodeType::STRING => function(StringValue $node) {return json_encode($node->value);}, - NodeType::BOOLEAN => function(BooleanValue $node) {return $node->value ? 'true' : 'false';}, - 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;}, + NodeType::INT => function(IntValue $node) { + return $node->value; + }, + NodeType::FLOAT => function(FloatValue $node) { + return $node->value; + }, + NodeType::STRING => function(StringValue $node) { + 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 NodeType::DIRECTIVE => function(Directive $node) { @@ -127,9 +146,15 @@ class Printer }, // Type - NodeType::NAMED_TYPE => function(NamedType $node) {return $node->name;}, - NodeType::LIST_TYPE => function(ListType $node) {return '[' . $node->type . ']';}, - NodeType::NON_NULL_TYPE => function(NonNullType $node) {return $node->type . '!';}, + NodeType::NAMED_TYPE => function(NamedType $node) { + return $node->name; + }, + NodeType::LIST_TYPE => function(ListType $node) { + return '[' . $node->type . ']'; + }, + NodeType::NON_NULL_TYPE => function(NonNullType $node) { + return $node->type . '!'; + }, // Type System Definitions NodeType::SCHEMA_DEFINITION => function(SchemaDefinition $def) { @@ -139,7 +164,9 @@ class Printer $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) { return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' '); diff --git a/src/Language/Visitor.php b/src/Language/Visitor.php index 589e720..5a69c9d 100644 --- a/src/Language/Visitor.php +++ b/src/Language/Visitor.php @@ -65,6 +65,7 @@ class Visitor NodeType::FLOAT => [], NodeType::STRING => [], NodeType::BOOLEAN => [], + NodeType::NULL => [], NodeType::ENUM => [], NodeType::LST => ['values'], NodeType::OBJECT => ['fields'], diff --git a/src/Type/Definition/FieldArgument.php b/src/Type/Definition/FieldArgument.php index 8385b06..7ce1b21 100644 --- a/src/Type/Definition/FieldArgument.php +++ b/src/Type/Definition/FieldArgument.php @@ -40,6 +40,11 @@ class FieldArgument */ private $resolvedType; + /** + * @var bool + */ + private $defaultValueExists = false; + /** * @param array $config * @return array @@ -62,17 +67,23 @@ class FieldArgument */ public function __construct(array $def) { - $def += [ - 'type' => null, - 'name' => null, - 'defaultValue' => null, - 'description' => null - ]; - - $this->type = $def['type']; - $this->name = $def['name']; - $this->description = $def['description']; - $this->defaultValue = $def['defaultValue']; + foreach ($def as $key => $value) { + switch ($key) { + case 'type': + $this->type = $value; + break; + case 'name': + $this->name = $value; + break; + case 'defaultValue': + $this->defaultValue = $value; + $this->defaultValueExists = true; + break; + case 'description': + $this->description = $value; + break; + } + } $this->config = $def; } @@ -87,4 +98,12 @@ class FieldArgument } return $this->resolvedType; } + + /** + * @return bool + */ + public function defaultValueExists() + { + return $this->defaultValueExists; + } } diff --git a/src/Type/Definition/InputObjectField.php b/src/Type/Definition/InputObjectField.php index aafe4bc..a60f951 100644 --- a/src/Type/Definition/InputObjectField.php +++ b/src/Type/Definition/InputObjectField.php @@ -27,6 +27,13 @@ class InputObjectField */ public $type; + /** + * Helps to differentiate when `defaultValue` is `null` and when it was not even set initially + * + * @var bool + */ + private $defaultValueExists = false; + /** * InputObjectField constructor. * @param array $opts @@ -34,7 +41,16 @@ class InputObjectField public function __construct(array $opts) { foreach ($opts as $k => $v) { - $this->{$k} = $v; + switch ($k) { + case 'defaultValue': + $this->defaultValue = $v; + $this->defaultValueExists = true; + break; + case 'defaultValueExists': + break; + default: + $this->{$k} = $v; + } } } @@ -45,4 +61,12 @@ class InputObjectField { return Type::resolve($this->type); } + + /** + * @return bool + */ + public function defaultValueExists() + { + return $this->defaultValueExists; + } } diff --git a/src/Utils/AST.php b/src/Utils/AST.php index b100a5b..190e6d2 100644 --- a/src/Utils/AST.php +++ b/src/Utils/AST.php @@ -8,6 +8,7 @@ use GraphQL\Language\AST\FloatValue; use GraphQL\Language\AST\IntValue; use GraphQL\Language\AST\ListValue; use GraphQL\Language\AST\Name; +use GraphQL\Language\AST\NullValue; use GraphQL\Language\AST\ObjectField; use GraphQL\Language\AST\ObjectValue; use GraphQL\Language\AST\StringValue; @@ -43,21 +44,24 @@ class AST * | Int | Int | * | Float | Int / Float | * | Mixed | Enum Value | + * | null | NullValue | * * @param $value * @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) { if ($type instanceof NonNull) { - // Note: we're not checking that the result is non-null. - // This function is not responsible for validating the input value. - return self::astFromValue($value, $type->getWrappedType()); + $astValue = self::astFromValue($value, $type->getWrappedType()); + if ($astValue instanceof NullValue) { + return null; + } + return $astValue; } if ($value === null) { - return null; + return new NullValue([]); } // 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 // in the PHP object according to the fields in the input type. 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))) { return null; } @@ -92,13 +97,29 @@ class AST } else { $fieldValue = isset($value->{$fieldName}) ? $value->{$fieldName} : null; } - $fieldValue = self::astFromValue($fieldValue, $field->getType()); - if ($fieldValue) { - $fieldASTs[] = new ObjectField([ - 'name' => new Name(['value' => $fieldName]), - 'value' => $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([ + 'name' => new Name(['value' => $fieldName]), + 'value' => $fieldNode + ]); + } } } return new ObjectValue(['fields' => $fieldASTs]); @@ -165,11 +186,12 @@ class AST * | String | String | * | Int / Float | Int / Float | * | Enum Value | Mixed | + * | Null Value | null | * * @param $valueAST * @param InputType $type * @param null $variables - * @return array|null + * @return array|null|\stdClass * @throws \Exception */ public static function valueFromAST($valueAST, InputType $type, $variables = null) @@ -182,14 +204,22 @@ class AST } 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) { $variableName = $valueAST->name->value; 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 // assuming that this query has been validated and the variable usage here @@ -199,19 +229,23 @@ class AST if ($type instanceof ListOfType) { $itemType = $type->getWrappedType(); - if ($valueAST instanceof ListValue) { - return array_map(function($itemAST) use ($itemType, $variables) { - return self::valueFromAST($itemAST, $itemType, $variables); - }, $valueAST->values); - } else { - return [self::valueFromAST($valueAST, $itemType, $variables)]; + $items = $valueAST instanceof ListValue ? $valueAST->values : [$valueAST]; + $result = []; + foreach ($items as $itemAST) { + $value = self::valueFromAST($itemAST, $itemType, $variables); + if ($value === NullValue::getNullValue()) { + $value = null; + } + $result[] = $value; } + return $result; } if ($type instanceof InputObjectType) { $fields = $type->getFields(); if (!$valueAST instanceof ObjectValue) { - return null; + // No valid return value. + return ; } $fieldASTs = Utils::keyMap($valueAST->fields, function($field) {return $field->name->value;}); $values = []; @@ -219,12 +253,19 @@ class AST $fieldAST = isset($fieldASTs[$field->name]) ? $fieldASTs[$field->name] : null; $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; } - if (null !== $fieldValue) { - $values[$field->name] = $fieldValue; - } + + $values[$field->name] = $fieldValue; } return $values; } diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index a890838..0fd0299 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -8,6 +8,7 @@ use GraphQL\Language\AST\Document; use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeType; +use GraphQL\Language\AST\NullValue; use GraphQL\Language\AST\Value; use GraphQL\Language\AST\Variable; use GraphQL\Language\Printer; @@ -155,7 +156,7 @@ class DocumentValidator // A value must be provided if the type is non-null. if ($type instanceof NonNull) { $wrappedType = $type->getWrappedType(); - if (!$valueAST) { + if (!$valueAST || $valueAST instanceof NullValue) { if ($wrappedType->name) { return [ "Expected \"{$wrappedType->name}!\", found null." ]; } @@ -164,7 +165,7 @@ class DocumentValidator return static::isValidLiteralValue($wrappedType, $valueAST); } - if (!$valueAST) { + if (!$valueAST || $valueAST instanceof NullValue) { return []; } diff --git a/tests/Executor/ExecutorTest.php b/tests/Executor/ExecutorTest.php index 07daaa3..12904b5 100644 --- a/tests/Executor/ExecutorTest.php +++ b/tests/Executor/ExecutorTest.php @@ -836,7 +836,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase $result = Executor::execute($schema, $query); $expected = [ '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"}}' ] ]; diff --git a/tests/Executor/VariablesTest.php b/tests/Executor/VariablesTest.php index d71f9b7..cb8cdb9 100644 --- a/tests/Executor/VariablesTest.php +++ b/tests/Executor/VariablesTest.php @@ -49,6 +49,28 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $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 $doc = ' { diff --git a/tests/Language/ParserTest.php b/tests/Language/ParserTest.php index 6977ba4..e4caa4d 100644 --- a/tests/Language/ParserTest.php +++ b/tests/Language/ParserTest.php @@ -6,6 +6,7 @@ use GraphQL\Language\AST\Field; use GraphQL\Language\AST\Name; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeType; +use GraphQL\Language\AST\NullValue; use GraphQL\Language\AST\SelectionSet; use GraphQL\Language\AST\StringValue; use GraphQL\Language\Parser; @@ -106,15 +107,6 @@ fragment MissingOn Type 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 */ @@ -393,6 +385,17 @@ fragment $fragmentName on Type { // 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 */ diff --git a/tests/Language/PrinterTest.php b/tests/Language/PrinterTest.php index a73d55e..bf584ee 100644 --- a/tests/Language/PrinterTest.php +++ b/tests/Language/PrinterTest.php @@ -154,7 +154,7 @@ fragment frag on Friend { } { - unnamed(truthy: true, falsey: false) + unnamed(truthy: true, falsey: false, nullish: null) query } diff --git a/tests/Language/SchemaPrinterTest.php b/tests/Language/SchemaPrinterTest.php index c0ead91..e704ef4 100644 --- a/tests/Language/SchemaPrinterTest.php +++ b/tests/Language/SchemaPrinterTest.php @@ -63,6 +63,7 @@ type Foo implements Bar { four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String six(argument: InputType = {key: "value"}): Type + seven(argument: Int = null): Type } type AnnotatedObject @onObject(arg: "value") { diff --git a/tests/Language/VisitorTest.php b/tests/Language/VisitorTest.php index 2710702..fd5fd33 100644 --- a/tests/Language/VisitorTest.php +++ b/tests/Language/VisitorTest.php @@ -636,6 +636,12 @@ class VisitorTest extends \PHPUnit_Framework_TestCase [ 'enter', 'BooleanValue', 'value', 'Argument' ], [ 'leave', 'BooleanValue', 'value', 'Argument' ], [ '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 ], [ 'enter', 'Field', 1, null ], [ 'enter', 'Name', 'name', 'Field' ], diff --git a/tests/Language/kitchen-sink.graphql b/tests/Language/kitchen-sink.graphql index 0e04e2e..993de9a 100644 --- a/tests/Language/kitchen-sink.graphql +++ b/tests/Language/kitchen-sink.graphql @@ -52,6 +52,6 @@ fragment frag on Friend { } { - unnamed(truthy: true, falsey: false), + unnamed(truthy: true, falsey: false, nullish: null), query } diff --git a/tests/Language/schema-kitchen-sink.graphql b/tests/Language/schema-kitchen-sink.graphql index d148276..a56c0e4 100644 --- a/tests/Language/schema-kitchen-sink.graphql +++ b/tests/Language/schema-kitchen-sink.graphql @@ -17,6 +17,7 @@ type Foo implements Bar { four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String six(argument: InputType = {key: "value"}): Type + seven(argument: Int = null): Type } type AnnotatedObject @onObject(arg: "value") { diff --git a/tests/Utils/AstFromValueTest.php b/tests/Utils/AstFromValueTest.php index a65762a..6468fb1 100644 --- a/tests/Utils/AstFromValueTest.php +++ b/tests/Utils/AstFromValueTest.php @@ -7,6 +7,7 @@ use GraphQL\Language\AST\FloatValue; use GraphQL\Language\AST\IntValue; use GraphQL\Language\AST\ListValue; use GraphQL\Language\AST\Name; +use GraphQL\Language\AST\NullValue; use GraphQL\Language\AST\ObjectField; use GraphQL\Language\AST\ObjectValue; 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' => 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' => 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' => '123']), AST::astFromValue(123, 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 IntValue(['value' => '123']), AST::astFromValue(123, 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)); } + 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 function complexValue() diff --git a/tests/Validator/ArgumentsOfCorrectTypeTest.php b/tests/Validator/ArgumentsOfCorrectTypeTest.php index e180d71..ed2175f 100644 --- a/tests/Validator/ArgumentsOfCorrectTypeTest.php +++ b/tests/Validator/ArgumentsOfCorrectTypeTest.php @@ -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 /** @@ -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 */ @@ -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 diff --git a/tests/Validator/DefaultValuesOfCorrectTypeTest.php b/tests/Validator/DefaultValuesOfCorrectTypeTest.php index 21aba50..3dbbe59 100644 --- a/tests/Validator/DefaultValuesOfCorrectTypeTest.php +++ b/tests/Validator/DefaultValuesOfCorrectTypeTest.php @@ -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 */