Improved enums (now they can handle complex values)

This commit is contained in:
vladar 2016-10-19 01:20:41 +07:00
parent 7f22d4b874
commit 0a182ac53c
4 changed files with 328 additions and 14 deletions

View File

@ -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);
} }
} }

View File

@ -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
View 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);
}
}
}
}

View File

@ -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));
if (is_array($err)) {
$this->assertEquals(
$err['message'],
$result->errors[0]->getMessage()
);
$this->assertEquals(
$err['locations'],
$result->errors[0]->getLocations()
);
} else {
$this->assertEquals( $this->assertEquals(
$err, $err,
$result->errors[0]->getMessage() $result->errors[0]->getMessage()
); );
} }
}
} }