mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-22 12:56:05 +03:00
Improved enums (now they can handle complex values)
This commit is contained in:
parent
7f22d4b874
commit
0a182ac53c
@ -44,13 +44,14 @@ class EnumType extends Type implements InputType, OutputType, LeafType
|
|||||||
|
|
||||||
if (!empty($config['values'])) {
|
if (!empty($config['values'])) {
|
||||||
foreach ($config['values'] as $name => $value) {
|
foreach ($config['values'] as $name => $value) {
|
||||||
$this->values[] = Utils::assign(new EnumValueDefinition(), $value + ['name' => $name, 'value' => $name]); // value will be equal to name only if 'value' is not set in definition
|
// value will be equal to name only if 'value' is not set in definition
|
||||||
|
$this->values[] = new EnumValueDefinition($value + ['name' => $name, 'value' => $name]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<EnumValueDefinition>
|
* @return EnumValueDefinition[]
|
||||||
*/
|
*/
|
||||||
public function getValues()
|
public function getValues()
|
||||||
{
|
{
|
||||||
@ -96,16 +97,15 @@ class EnumType extends Type implements InputType, OutputType, LeafType
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo Value lookup for any type, not just scalars
|
* @return Utils\MixedStore<mixed, EnumValueDefinition>
|
||||||
* @return \ArrayObject<mixed, EnumValueDefinition>
|
|
||||||
*/
|
*/
|
||||||
private function getValueLookup()
|
private function getValueLookup()
|
||||||
{
|
{
|
||||||
if (null === $this->valueLookup) {
|
if (null === $this->valueLookup) {
|
||||||
$this->valueLookup = new \ArrayObject();
|
$this->valueLookup = new Utils\MixedStore();
|
||||||
|
|
||||||
foreach ($this->getValues() as $valueName => $value) {
|
foreach ($this->getValues() as $valueName => $value) {
|
||||||
$this->valueLookup[$value->value] = $value;
|
$this->valueLookup->offsetSet($value->value, $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace GraphQL\Type\Definition;
|
namespace GraphQL\Type\Definition;
|
||||||
|
use GraphQL\Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class EnumValueDefinition
|
* Class EnumValueDefinition
|
||||||
@ -26,4 +27,17 @@ class EnumValueDefinition
|
|||||||
* @var string|null
|
* @var string|null
|
||||||
*/
|
*/
|
||||||
public $description;
|
public $description;
|
||||||
|
|
||||||
|
public function __construct(array $config)
|
||||||
|
{
|
||||||
|
Utils::assign($this, $config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isDeprecated()
|
||||||
|
{
|
||||||
|
return !!$this->deprecationReason;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
170
src/Utils/MixedStore.php
Normal file
170
src/Utils/MixedStore.php
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Utils;
|
||||||
|
use GraphQL\Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to PHP array, but allows any type of data to act as key (including arrays, objects, scalars)
|
||||||
|
*
|
||||||
|
* Note: unfortunately when storing array as key - access and modification is O(N)
|
||||||
|
* (yet this should be really rare case and should be avoided when possible)
|
||||||
|
*
|
||||||
|
* Class MixedStore
|
||||||
|
* @package GraphQL\Utils
|
||||||
|
*/
|
||||||
|
class MixedStore implements \ArrayAccess
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $scalarStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \SplObjectStorage
|
||||||
|
*/
|
||||||
|
private $objectStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $arrayKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $arrayValues;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $lastArrayKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var mixed
|
||||||
|
*/
|
||||||
|
private $lastArrayValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MixedStore constructor.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->scalarStore = [];
|
||||||
|
$this->objectStore = new \SplObjectStorage();
|
||||||
|
$this->arrayKeys = [];
|
||||||
|
$this->arrayValues = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a offset exists
|
||||||
|
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
|
||||||
|
* @param mixed $offset <p>
|
||||||
|
* An offset to check for.
|
||||||
|
* </p>
|
||||||
|
* @return boolean true on success or false on failure.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* The return value will be casted to boolean if non-boolean was returned.
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
public function offsetExists($offset)
|
||||||
|
{
|
||||||
|
if (is_scalar($offset)) {
|
||||||
|
return array_key_exists($offset, $this->scalarStore);
|
||||||
|
}
|
||||||
|
if (is_object($offset)) {
|
||||||
|
return $this->objectStore->offsetExists($offset);
|
||||||
|
}
|
||||||
|
if (is_array($offset)) {
|
||||||
|
foreach ($this->arrayKeys as $index => $entry) {
|
||||||
|
if ($entry === $offset) {
|
||||||
|
$this->lastArrayKey = $offset;
|
||||||
|
$this->lastArrayValue = $this->arrayValues[$index];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset to retrieve
|
||||||
|
* @link http://php.net/manual/en/arrayaccess.offsetget.php
|
||||||
|
* @param mixed $offset <p>
|
||||||
|
* The offset to retrieve.
|
||||||
|
* </p>
|
||||||
|
* @return mixed Can return all value types.
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
public function offsetGet($offset)
|
||||||
|
{
|
||||||
|
if (is_scalar($offset)) {
|
||||||
|
return $this->scalarStore[$offset];
|
||||||
|
}
|
||||||
|
if (is_object($offset)) {
|
||||||
|
return $this->objectStore->offsetGet($offset);
|
||||||
|
}
|
||||||
|
if (is_array($offset)) {
|
||||||
|
// offsetGet is often called directly after offsetExists, so optimize to avoid second loop:
|
||||||
|
if ($this->lastArrayKey === $offset) {
|
||||||
|
return $this->lastArrayValue;
|
||||||
|
}
|
||||||
|
foreach ($this->arrayKeys as $index => $entry) {
|
||||||
|
if ($entry === $offset) {
|
||||||
|
return $this->arrayValues[$index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset to set
|
||||||
|
* @link http://php.net/manual/en/arrayaccess.offsetset.php
|
||||||
|
* @param mixed $offset <p>
|
||||||
|
* The offset to assign the value to.
|
||||||
|
* </p>
|
||||||
|
* @param mixed $value <p>
|
||||||
|
* The value to set.
|
||||||
|
* </p>
|
||||||
|
* @return void
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
public function offsetSet($offset, $value)
|
||||||
|
{
|
||||||
|
if (is_scalar($offset)) {
|
||||||
|
$this->scalarStore[$offset] = $value;
|
||||||
|
} else if (is_object($offset)) {
|
||||||
|
$this->objectStore[$offset] = $value;
|
||||||
|
} else if (is_array($offset)) {
|
||||||
|
$this->arrayKeys[] = $offset;
|
||||||
|
$this->arrayValues[] = $value;
|
||||||
|
} else {
|
||||||
|
throw new \InvalidArgumentException("Unexpected offset type: " . Utils::printSafe($offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset to unset
|
||||||
|
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
|
||||||
|
* @param mixed $offset <p>
|
||||||
|
* The offset to unset.
|
||||||
|
* </p>
|
||||||
|
* @return void
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
public function offsetUnset($offset)
|
||||||
|
{
|
||||||
|
if (is_scalar($offset)) {
|
||||||
|
unset($this->scalarStore[$offset]);
|
||||||
|
} else if (is_object($offset)) {
|
||||||
|
$this->objectStore->offsetUnset($offset);
|
||||||
|
} else if (is_array($offset)) {
|
||||||
|
$index = array_search($offset, $this->arrayKeys, true);
|
||||||
|
|
||||||
|
if (false !== $index) {
|
||||||
|
array_splice($this->arrayKeys, $index, 1);
|
||||||
|
array_splice($this->arrayValues, $index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,15 +3,29 @@ namespace GraphQL\Tests\Type;
|
|||||||
|
|
||||||
use GraphQL\Error;
|
use GraphQL\Error;
|
||||||
use GraphQL\GraphQL;
|
use GraphQL\GraphQL;
|
||||||
|
use GraphQL\Language\SourceLocation;
|
||||||
use GraphQL\Schema;
|
use GraphQL\Schema;
|
||||||
use GraphQL\Type\Definition\EnumType;
|
use GraphQL\Type\Definition\EnumType;
|
||||||
use GraphQL\Type\Definition\ObjectType;
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
use GraphQL\Type\Definition\Type;
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use GraphQL\Type\Introspection;
|
||||||
|
|
||||||
class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var Schema
|
||||||
|
*/
|
||||||
private $schema;
|
private $schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var EnumType
|
||||||
|
*/
|
||||||
|
private $ComplexEnum;
|
||||||
|
|
||||||
|
private $Complex1;
|
||||||
|
|
||||||
|
private $Complex2;
|
||||||
|
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
$ColorType = new EnumType([
|
$ColorType = new EnumType([
|
||||||
@ -23,6 +37,17 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
|||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$Complex1 = ['someRandomFunction' => function() {}];
|
||||||
|
$Complex2 = new \ArrayObject(['someRandomValue' => 123]);
|
||||||
|
|
||||||
|
$ComplexEnum = new EnumType([
|
||||||
|
'name' => 'Complex',
|
||||||
|
'values' => [
|
||||||
|
'ONE' => ['value' => $Complex1],
|
||||||
|
'TWO' => ['value' => $Complex2]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
$QueryType = new ObjectType([
|
$QueryType = new ObjectType([
|
||||||
'name' => 'Query',
|
'name' => 'Query',
|
||||||
'fields' => [
|
'fields' => [
|
||||||
@ -59,6 +84,36 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
|||||||
return $args['fromEnum'];
|
return $args['fromEnum'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
'complexEnum' => [
|
||||||
|
'type' => $ComplexEnum,
|
||||||
|
'args' => [
|
||||||
|
'fromEnum' => [
|
||||||
|
'type' => $ComplexEnum,
|
||||||
|
// Note: defaultValue is provided an *internal* representation for
|
||||||
|
// Enums, rather than the string name.
|
||||||
|
'defaultValue' => $Complex1
|
||||||
|
],
|
||||||
|
'provideGoodValue' => [
|
||||||
|
'type' => Type::boolean(),
|
||||||
|
],
|
||||||
|
'provideBadValue' => [
|
||||||
|
'type' => Type::boolean()
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'resolve' => function($value, $args) use ($Complex1, $Complex2) {
|
||||||
|
if (!empty($args['provideGoodValue'])) {
|
||||||
|
// Note: this is one of the references of the internal values which
|
||||||
|
// ComplexEnum allows.
|
||||||
|
return $Complex2;
|
||||||
|
}
|
||||||
|
if (!empty($args['provideBadValue'])) {
|
||||||
|
// Note: similar shape, but not the same *reference*
|
||||||
|
// as Complex2 above. Enum internal values require === equality.
|
||||||
|
return new \ArrayObject(['someRandomValue' => 123]);
|
||||||
|
}
|
||||||
|
return $args['fromEnum'];
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
@ -89,6 +144,10 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
|||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->Complex1 = $Complex1;
|
||||||
|
$this->Complex2 = $Complex2;
|
||||||
|
$this->ComplexEnum = $ComplexEnum;
|
||||||
|
|
||||||
$this->schema = new Schema([
|
$this->schema = new Schema([
|
||||||
'query' => $QueryType,
|
'query' => $QueryType,
|
||||||
'mutation' => $MutationType,
|
'mutation' => $MutationType,
|
||||||
@ -139,7 +198,10 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->expectFailure(
|
$this->expectFailure(
|
||||||
'{ colorEnum(fromEnum: "GREEN") }',
|
'{ colorEnum(fromEnum: "GREEN") }',
|
||||||
null,
|
null,
|
||||||
"Argument \"fromEnum\" has invalid value \"GREEN\".\nExpected type \"Color\", found \"GREEN\"."
|
[
|
||||||
|
'message' => "Argument \"fromEnum\" has invalid value \"GREEN\".\nExpected type \"Color\", found \"GREEN\".",
|
||||||
|
'locations' => [new SourceLocation(1, 23)]
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,9 +210,13 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
|||||||
*/
|
*/
|
||||||
public function testDoesNotAcceptIncorrectInternalValue()
|
public function testDoesNotAcceptIncorrectInternalValue()
|
||||||
{
|
{
|
||||||
$this->assertEquals(
|
$this->expectFailure(
|
||||||
['data' => ['colorEnum' => null]],
|
'{ colorEnum(fromString: "GREEN") }',
|
||||||
GraphQL::execute($this->schema, '{ colorEnum(fromString: "GREEN") }')
|
null,
|
||||||
|
[
|
||||||
|
'message' => 'Expected a value of type "Color" but received: GREEN',
|
||||||
|
'locations' => [new SourceLocation(1, 3)]
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,14 +360,78 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @it may present a values API for complex enums
|
||||||
|
*/
|
||||||
|
public function testMayPresentValuesAPIForComplexEnums()
|
||||||
|
{
|
||||||
|
$ComplexEnum = $this->ComplexEnum;
|
||||||
|
$values = $ComplexEnum->getValues();
|
||||||
|
|
||||||
|
$this->assertEquals(2, count($values));
|
||||||
|
$this->assertEquals('ONE', $values[0]->name);
|
||||||
|
$this->assertEquals($this->Complex1, $values[0]->value);
|
||||||
|
$this->assertEquals('TWO', $values[1]->name);
|
||||||
|
$this->assertEquals($this->Complex2, $values[1]->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @it may be internally represented with complex values
|
||||||
|
*/
|
||||||
|
public function testMayBeInternallyRepresentedWithComplexValues()
|
||||||
|
{
|
||||||
|
$result = GraphQL::execute($this->schema, '{
|
||||||
|
first: complexEnum
|
||||||
|
second: complexEnum(fromEnum: TWO)
|
||||||
|
good: complexEnum(provideGoodValue: true)
|
||||||
|
bad: complexEnum(provideBadValue: true)
|
||||||
|
}');
|
||||||
|
|
||||||
|
$expected = [
|
||||||
|
'data' => [
|
||||||
|
'first' => 'ONE',
|
||||||
|
'second' => 'TWO',
|
||||||
|
'good' => 'TWO',
|
||||||
|
'bad' => null
|
||||||
|
],
|
||||||
|
'errors' => [[
|
||||||
|
'message' =>
|
||||||
|
'Expected a value of type "Complex" but received: instance of ArrayObject',
|
||||||
|
'locations' => [['line' => 5, 'column' => 9]]
|
||||||
|
]]
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @it can be introspected without error
|
||||||
|
*/
|
||||||
|
public function testCanBeIntrospectedWithoutError()
|
||||||
|
{
|
||||||
|
$result = GraphQL::execute($this->schema, Introspection::getIntrospectionQuery());
|
||||||
|
$this->assertArrayNotHasKey('errors', $result);
|
||||||
|
}
|
||||||
|
|
||||||
private function expectFailure($query, $vars, $err)
|
private function expectFailure($query, $vars, $err)
|
||||||
{
|
{
|
||||||
$result = GraphQL::executeAndReturnResult($this->schema, $query, null, null, $vars);
|
$result = GraphQL::executeAndReturnResult($this->schema, $query, null, null, $vars);
|
||||||
$this->assertEquals(1, count($result->errors));
|
$this->assertEquals(1, count($result->errors));
|
||||||
|
|
||||||
$this->assertEquals(
|
if (is_array($err)) {
|
||||||
$err,
|
$this->assertEquals(
|
||||||
$result->errors[0]->getMessage()
|
$err['message'],
|
||||||
);
|
$result->errors[0]->getMessage()
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
$err['locations'],
|
||||||
|
$result->errors[0]->getLocations()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->assertEquals(
|
||||||
|
$err,
|
||||||
|
$result->errors[0]->getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user