From 884a8967f3a97fc9a709f87f1d23a054aed61a4f Mon Sep 17 00:00:00 2001 From: Vladimir Razuvaev Date: Mon, 14 Aug 2017 19:42:01 +0700 Subject: [PATCH] Type loader tests --- src/Type/Schema.php | 83 ++++----- tests/Type/TypeLoaderTest.php | 319 ++++++++++++++++++++++++++++++++++ 2 files changed, 361 insertions(+), 41 deletions(-) create mode 100644 tests/Type/TypeLoaderTest.php diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 3623da4..72a7dce 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -185,44 +185,51 @@ class Schema */ public function getType($name) { - return $this->resolveType($name); + if (!isset($this->resolvedTypes[$name])) { + $this->resolvedTypes[$name] = $this->loadType($name); + } + return $this->resolvedTypes[$name]; } + /** + * @return array + */ private function collectAllTypes() { - $initialTypes = array_merge( - array_values($this->resolvedTypes), - [Introspection::_schema()] - ); + $initialTypes = $this->resolvedTypes; $typeMap = []; foreach ($initialTypes as $type) { $typeMap = TypeInfo::extractTypes($type, $typeMap); } - $types = $this->config->types; - if (is_callable($types)) { - $types = $types(); + if ($this->config->types) { + $types = $this->config->types; - Utils::invariant( - is_array($types) || $types instanceof \Traversable, - 'Schema types callable must return array or instance of Traversable but got: %s', - Utils::getVariableType($types) - ); - } + if (is_callable($types)) { + $types = $types(); + } - if (!empty($types)) { - foreach ($types as $type) { - Utils::invariant( - $type instanceof Type, - 'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but got: %s', + if (!is_array($types) && !$types instanceof \Traversable) { + throw new InvariantViolation(sprintf( + 'Schema types callable must return array or instance of Traversable but got: %s', Utils::getVariableType($types) - ); + )); + } + + foreach ($types as $index => $type) { + if (!$type instanceof Type) { + throw new InvariantViolation( + 'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but entry at %s is %s', + $index, + Utils::printSafe($type) + ); + } $typeMap = TypeInfo::extractTypes($type, $typeMap); } } - return $typeMap + Type::getInternalTypes(); + return $typeMap + Type::getInternalTypes() + Introspection::getTypes(); } /** @@ -235,7 +242,7 @@ class Schema public function getPossibleTypes(AbstractType $abstractType) { $possibleTypeMap = $this->getPossibleTypeMap(); - return array_values($possibleTypeMap[$abstractType->name]); + return isset($possibleTypeMap[$abstractType->name]) ? array_values($possibleTypeMap[$abstractType->name]) : []; } /** @@ -261,26 +268,9 @@ class Schema } /** - * Accepts name of type or type instance and returns type instance. If type with given name is not loaded yet - - * will load it first. - * - * @param $typeOrName + * @param $typeName * @return Type */ - public function resolveType($typeOrName) - { - if ($typeOrName instanceof Type) { - if ($typeOrName->name && !isset($this->resolvedTypes[$typeOrName->name])) { - $this->resolvedTypes[$typeOrName->name] = $typeOrName; - } - return $typeOrName; - } - if (!isset($this->resolvedTypes[$typeOrName])) { - $this->resolvedTypes[$typeOrName] = $this->loadType($typeOrName); - } - return $this->resolvedTypes[$typeOrName]; - } - private function loadType($typeName) { $typeLoader = $this->config->typeLoader; @@ -290,7 +280,18 @@ class Schema } $type = $typeLoader($typeName); - // TODO: validate returned value + + if (!$type instanceof Type) { + throw new InvariantViolation( + "Type loader is expected to return valid type \"$typeName\", but it returned " . Utils::printSafe($type) + ); + } + if ($type->name !== $typeName) { + throw new InvariantViolation( + "Type loader is expected to return type \"$typeName\", but it returned \"{$type->name}\"" + ); + } + return $type; } diff --git a/tests/Type/TypeLoaderTest.php b/tests/Type/TypeLoaderTest.php new file mode 100644 index 0000000..14787b4 --- /dev/null +++ b/tests/Type/TypeLoaderTest.php @@ -0,0 +1,319 @@ +calls = []; + + $this->node = new InterfaceType([ + 'name' => 'Node', + 'fields' => function() { + $this->calls[] = 'Node.fields'; + return [ + 'id' => Type::string() + ]; + }, + 'resolveType' => function() {} + ]); + + $this->content = new InterfaceType([ + 'name' => 'Content', + 'fields' => function() { + $this->calls[] = 'Content.fields'; + return [ + 'title' => Type::string(), + 'body' => Type::string(), + ]; + }, + 'resolveType' => function() {} + ]); + + $this->blogStory = new ObjectType([ + 'name' => 'BlogStory', + 'interfaces' => [ + $this->node, + $this->content + ], + 'fields' => function() { + $this->calls[] = 'BlogStory.fields'; + return [ + $this->node->getField('id'), + $this->content->getField('title'), + $this->content->getField('body'), + ]; + }, + ]); + + $this->query = new ObjectType([ + 'name' => 'Query', + 'fields' => function() { + $this->calls[] = 'Query.fields'; + return [ + 'latestContent' => $this->content, + 'node' => $this->node, + ]; + } + ]); + + $this->mutation = new ObjectType([ + 'name' => 'Mutation', + 'fields' => function() { + $this->calls[] = 'Mutation.fields'; + return [ + 'postStory' => [ + 'type' => $this->postStoryMutation, + 'args' => [ + 'input' => Type::nonNull($this->postStoryMutationInput), + 'clientRequestId' => Type::string() + ] + ] + ]; + } + ]); + + $this->postStoryMutation = new ObjectType([ + 'name' => 'PostStoryMutation', + 'fields' => [ + 'story' => $this->blogStory + ] + ]); + + $this->postStoryMutationInput = new InputObjectType([ + 'name' => 'PostStoryMutationInput', + 'fields' => [ + 'title' => Type::string(), + 'body' => Type::string(), + 'author' => Type::id(), + 'category' => Type::id() + ] + ]); + + $this->typeLoader = function($name) { + $this->calls[] = $name; + $prop = lcfirst($name); + return isset($this->{$prop}) ? $this->{$prop} : null; + }; + } + + public function testSchemaAcceptsTypeLoader() + { + new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => ['a' => Type::string()] + ]), + 'typeLoader' => function() {} + ]); + } + + public function testSchemaRejectsNonCallableTypeLoader() + { + $this->setExpectedException( + InvariantViolation::class, + 'Schema type loader must be callable if provided but got: array(0)' + ); + + new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => ['a' => Type::string()] + ]), + 'typeLoader' => [] + ]); + } + + public function testWorksWithoutTypeLoader() + { + $schema = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + 'types' => [$this->blogStory] + ]); + + $expected = [ + 'Query.fields', + 'Content.fields', + 'Node.fields', + 'Mutation.fields', + 'BlogStory.fields', + ]; + $this->assertEquals($expected, $this->calls); + + $this->assertSame($this->query, $schema->getType('Query')); + $this->assertSame($this->mutation, $schema->getType('Mutation')); + $this->assertSame($this->node, $schema->getType('Node')); + $this->assertSame($this->content, $schema->getType('Content')); + $this->assertSame($this->blogStory, $schema->getType('BlogStory')); + $this->assertSame($this->postStoryMutation, $schema->getType('PostStoryMutation')); + $this->assertSame($this->postStoryMutationInput, $schema->getType('PostStoryMutationInput')); + + $expectedTypeMap = [ + 'Query' => $this->query, + 'Mutation' => $this->mutation, + 'Node' => $this->node, + 'String' => Type::string(), + 'Content' => $this->content, + 'BlogStory' => $this->blogStory, + 'PostStoryMutationInput' => $this->postStoryMutationInput, + ]; + + $this->assertArraySubset($expectedTypeMap, $schema->getTypeMap()); + } + + public function testWorksWithTypeLoader() + { + $schema = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + 'typeLoader' => $this->typeLoader + ]); + $this->assertEquals([], $this->calls); + + $node = $schema->getType('Node'); + $this->assertSame($this->node, $node); + $this->assertEquals(['Node'], $this->calls); + + $content = $schema->getType('Content'); + $this->assertSame($this->content, $content); + $this->assertEquals(['Node', 'Content'], $this->calls); + + $input = $schema->getType('PostStoryMutationInput'); + $this->assertSame($this->postStoryMutationInput, $input); + $this->assertEquals(['Node', 'Content', 'PostStoryMutationInput'], $this->calls); + } + + public function testOnlyCallsLoaderOnce() + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => $this->typeLoader + ]); + + $schema->getType('Node'); + $this->assertEquals(['Node'], $this->calls); + + $schema->getType('Node'); + $this->assertEquals(['Node'], $this->calls); + } + + public function testFailsOnNonExistentType() + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => function() {} + ]); + + $this->setExpectedException( + InvariantViolation::class, + 'Type loader is expected to return valid type "NonExistingType", but it returned null' + ); + + $schema->getType('NonExistingType'); + } + + public function testFailsOnNonType() + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => function() { + return new \stdClass(); + } + ]); + + $this->setExpectedException( + InvariantViolation::class, + 'Type loader is expected to return valid type "Node", but it returned instance of stdClass' + ); + + $schema->getType('Node'); + } + + public function testFailsOnInvalidLoad() + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => function() { + return $this->content; + } + ]); + + $this->setExpectedException( + InvariantViolation::class, + 'Type loader is expected to return type "Node", but it returned "Content"' + ); + + $schema->getType('Node'); + } + + public function testPassesThroughAnExceptionInLoader() + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => function() { + throw new \Exception("This is the exception we are looking for"); + } + ]); + + $this->setExpectedException( + \Exception::class, + 'This is the exception we are looking for' + ); + + $schema->getType('Node'); + } +}