mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-25 14:26:08 +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'])) {
|
||||
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()
|
||||
{
|
||||
@ -96,16 +97,15 @@ class EnumType extends Type implements InputType, OutputType, LeafType
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Value lookup for any type, not just scalars
|
||||
* @return \ArrayObject<mixed, EnumValueDefinition>
|
||||
* @return Utils\MixedStore<mixed, EnumValueDefinition>
|
||||
*/
|
||||
private function getValueLookup()
|
||||
{
|
||||
if (null === $this->valueLookup) {
|
||||
$this->valueLookup = new \ArrayObject();
|
||||
$this->valueLookup = new Utils\MixedStore();
|
||||
|
||||
foreach ($this->getValues() as $valueName => $value) {
|
||||
$this->valueLookup[$value->value] = $value;
|
||||
$this->valueLookup->offsetSet($value->value, $value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
use GraphQL\Utils;
|
||||
|
||||
/**
|
||||
* Class EnumValueDefinition
|
||||
@ -26,4 +27,17 @@ class EnumValueDefinition
|
||||
* @var string|null
|
||||
*/
|
||||
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\GraphQL;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Introspection;
|
||||
|
||||
class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @var Schema
|
||||
*/
|
||||
private $schema;
|
||||
|
||||
/**
|
||||
* @var EnumType
|
||||
*/
|
||||
private $ComplexEnum;
|
||||
|
||||
private $Complex1;
|
||||
|
||||
private $Complex2;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$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([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
@ -59,6 +84,36 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
||||
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([
|
||||
'query' => $QueryType,
|
||||
'mutation' => $MutationType,
|
||||
@ -139,7 +198,10 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
||||
$this->expectFailure(
|
||||
'{ colorEnum(fromEnum: "GREEN") }',
|
||||
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()
|
||||
{
|
||||
$this->assertEquals(
|
||||
['data' => ['colorEnum' => null]],
|
||||
GraphQL::execute($this->schema, '{ colorEnum(fromString: "GREEN") }')
|
||||
$this->expectFailure(
|
||||
'{ 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)
|
||||
{
|
||||
$result = GraphQL::executeAndReturnResult($this->schema, $query, null, null, $vars);
|
||||
$this->assertEquals(1, count($result->errors));
|
||||
|
||||
$this->assertEquals(
|
||||
$err,
|
||||
$result->errors[0]->getMessage()
|
||||
);
|
||||
if (is_array($err)) {
|
||||
$this->assertEquals(
|
||||
$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