From 0a182ac53cf2c1aeac5870241d0d57d46a56b122 Mon Sep 17 00:00:00 2001 From: vladar Date: Wed, 19 Oct 2016 01:20:41 +0700 Subject: [PATCH] Improved enums (now they can handle complex values) --- src/Type/Definition/EnumType.php | 12 +- src/Type/Definition/EnumValueDefinition.php | 14 ++ src/Utils/MixedStore.php | 170 ++++++++++++++++++++ tests/Type/EnumTypeTest.php | 146 ++++++++++++++++- 4 files changed, 328 insertions(+), 14 deletions(-) create mode 100644 src/Utils/MixedStore.php diff --git a/src/Type/Definition/EnumType.php b/src/Type/Definition/EnumType.php index cd7ba22..ee47342 100644 --- a/src/Type/Definition/EnumType.php +++ b/src/Type/Definition/EnumType.php @@ -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 + * @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 + * @return Utils\MixedStore */ 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); } } diff --git a/src/Type/Definition/EnumValueDefinition.php b/src/Type/Definition/EnumValueDefinition.php index 2afc09a..a28ce88 100644 --- a/src/Type/Definition/EnumValueDefinition.php +++ b/src/Type/Definition/EnumValueDefinition.php @@ -1,5 +1,6 @@ deprecationReason; + } } diff --git a/src/Utils/MixedStore.php b/src/Utils/MixedStore.php new file mode 100644 index 0000000..9811ba7 --- /dev/null +++ b/src/Utils/MixedStore.php @@ -0,0 +1,170 @@ +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

+ * An offset to check for. + *

+ * @return boolean true on success or false on failure. + *

+ *

+ * 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

+ * The offset to retrieve. + *

+ * @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

+ * The offset to assign the value to. + *

+ * @param mixed $value

+ * The value to set. + *

+ * @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

+ * The offset to unset. + *

+ * @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); + } + } + } +} diff --git a/tests/Type/EnumTypeTest.php b/tests/Type/EnumTypeTest.php index 35eb8c5..1d637e0 100644 --- a/tests/Type/EnumTypeTest.php +++ b/tests/Type/EnumTypeTest.php @@ -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() + ); + } } }