diff --git a/src/Type/Definition/CustomScalarType.php b/src/Type/Definition/CustomScalarType.php new file mode 100644 index 0000000..0893c27 --- /dev/null +++ b/src/Type/Definition/CustomScalarType.php @@ -0,0 +1,30 @@ +name = $config['name']; + $this->config = $config; + parent::__construct(); + } + + public function serialize($value) + { + return call_user_func($this->config['serialize'], $value); + } + + public function parseValue($value) + { + return call_user_func($this->config['parseValue'], $value); + } + + public function parseLiteral(/* GraphQL\Language\AST\Value */ $valueAST) + { + return call_user_func($this->config['parseLiteral'], $valueAST); + } +} diff --git a/tests/Type/DefinitionTest.php b/tests/Type/DefinitionTest.php index ef0de37..767535d 100644 --- a/tests/Type/DefinitionTest.php +++ b/tests/Type/DefinitionTest.php @@ -39,6 +39,11 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase */ public $blogQuery; + /** + * @var ObjectType + */ + public $blogSubscription; + /** * @var ObjectType */ @@ -124,11 +129,28 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase 'writeArticle' => ['type' => $this->blogArticle] ] ]); + + $this->blogSubscription = new ObjectType([ + 'name' => 'Subscription', + 'fields' => [ + 'articleSubscribe' => [ + 'args' => [ 'id' => [ 'type' => Type::string() ]], + 'type' => $this->blogArticle + ] + ] + ]); } + // Type System: Example + + /** + * @it defines a query only schema + */ public function testDefinesAQueryOnlySchema() { - $blogSchema = new Schema($this->blogQuery); + $blogSchema = new Schema([ + 'query' => $this->blogQuery + ]); $this->assertSame($blogSchema->getQueryType(), $this->blogQuery); @@ -165,9 +187,15 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase $this->assertSame($this->blogArticle, $feedFieldType->getWrappedType()); } + /** + * @it defines a mutation schema + */ public function testDefinesAMutationSchema() { - $schema = new Schema($this->blogQuery, $this->blogMutation); + $schema = new Schema([ + 'query' => $this->blogQuery, + 'mutation' => $this->blogMutation + ]); $this->assertSame($this->blogMutation, $schema->getMutationType()); $writeMutation = $this->blogMutation->getField('writeArticle'); @@ -178,6 +206,27 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase $this->assertSame('writeArticle', $writeMutation->name); } + /** + * @it defines a subscription schema + */ + public function testDefinesSubscriptionSchema() + { + $schema = new Schema([ + 'query' => $this->blogQuery, + 'subscription' => $this->blogSubscription + ]); + + $this->assertEquals($this->blogSubscription, $schema->getSubscriptionType()); + + $sub = $this->blogSubscription->getField('articleSubscribe'); + $this->assertEquals($sub->getType(), $this->blogArticle); + $this->assertEquals($sub->getType()->name, 'Article'); + $this->assertEquals($sub->name, 'articleSubscribe'); + } + + /** + * @it includes nested input objects in the map + */ public function testIncludesNestedInputObjectInTheMap() { $nestedInputObject = new InputObjectType([ @@ -198,10 +247,16 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase ] ]); - $schema = new Schema($this->blogQuery, $someMutation); + $schema = new Schema([ + 'query' => $this->blogQuery, + 'mutation' => $someMutation + ]); $this->assertSame($nestedInputObject, $schema->getType('NestedInputObject')); } + /** + * @it includes interfaces\' subtypes in the type map + */ public function testIncludesInterfaceSubtypesInTheTypeMap() { $someInterface = new InterfaceType([ @@ -220,18 +275,23 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase 'isTypeOf' => function() {return true;} ]); - $schema = new Schema(new ObjectType([ - 'name' => 'Query', - 'fields' => [ - 'iface' => ['type' => $someInterface] - ] - ])); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'iface' => ['type' => $someInterface] + ] + ]), + 'types' => [$someSubtype] + ]); $this->assertSame($someSubtype, $schema->getType('SomeSubtype')); } + /** + * @it includes interfaces\' thunk subtypes in the type map + */ public function testIncludesInterfacesThunkSubtypesInTheTypeMap() { - // includes interfaces' thunk subtypes in the type map $someInterface = null; $someSubtype = new ObjectType([ @@ -250,16 +310,22 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase ] ]); - $schema = new Schema(new ObjectType([ - 'name' => 'Query', - 'fields' => [ - 'iface' => ['type' => $someInterface] - ] - ])); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'iface' => ['type' => $someInterface] + ] + ]), + 'types' => [$someSubtype] + ]); $this->assertSame($someSubtype, $schema->getType('SomeSubtype')); } + /** + * @it stringifies simple types + */ public function testStringifiesSimpleTypes() { $this->assertSame('Int', (string) Type::int()); @@ -278,6 +344,9 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase $this->assertSame('[[Int]]', (string) new ListOfType(new ListOfType(Type::int()))); } + /** + * @it identifies input types + */ public function testIdentifiesInputTypes() { $expected = [ @@ -294,6 +363,9 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase } } + /** + * @it identifies output types + */ public function testIdentifiesOutputTypes() { $expected = [ @@ -310,12 +382,18 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase } } + /** + * @it prohibits nesting NonNull inside NonNull + */ public function testProhibitsNonNullNesting() { $this->setExpectedException('\Exception'); new NonNull(new NonNull(Type::int())); } + /** + * @it prohibits putting non-Object types in unions + */ public function testProhibitsPuttingNonObjectTypesInUnions() { $int = Type::int(); @@ -387,12 +465,15 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase } ]); - $schema = new Schema(new ObjectType([ - 'name' => 'Query', - 'fields' => [ - 'node' => ['type' => $node] - ] - ])); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'node' => ['type' => $node] + ] + ]), + 'types' => [$user, $blog] + ]); $this->assertTrue($called); @@ -429,7 +510,10 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase ] ]); - $schema = new Schema($this->blogQuery, $someMutation); + $schema = new Schema([ + 'query' => $this->blogQuery, + 'mutation' => $someMutation + ]); $this->assertTrue($called); $this->assertSame($inputObject, $schema->getType('InputObject')); @@ -459,7 +543,9 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase ] ]); - $schema = new Schema($query); + $schema = new Schema([ + 'query' => $query + ]); $this->assertTrue($called); $this->assertSame($interface, $schema->getType('SomeInterface')); diff --git a/tests/Type/EnumTypeTest.php b/tests/Type/EnumTypeTest.php new file mode 100644 index 0000000..a7f2be2 --- /dev/null +++ b/tests/Type/EnumTypeTest.php @@ -0,0 +1,306 @@ + 'Color', + 'values' => [ + 'RED' => ['value' => 0], + 'GREEN' => ['value' => 1], + 'BLUE' => ['value' => 2], + ] + ]); + + $QueryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'colorEnum' => [ + 'type' => $ColorType, + 'args' => [ + 'fromEnum' => ['type' => $ColorType], + 'fromInt' => ['type' => Type::int()], + 'fromString' => ['type' => Type::string()], + ], + 'resolve' => function ($value, $args) { + if (isset($args['fromInt'])) { + return $args['fromInt']; + } + if (isset($args['fromString'])) { + return $args['fromString']; + } + if (isset($args['fromEnum'])) { + return $args['fromEnum']; + } + } + ], + 'colorInt' => [ + 'type' => Type::int(), + 'args' => [ + 'fromEnum' => ['type' => $ColorType], + 'fromInt' => ['type' => Type::int()], + ], + 'resolve' => function ($value, $args) { + if (isset($args['fromInt'])) { + return $args['fromInt']; + } + if (isset($args['fromEnum'])) { + return $args['fromEnum']; + } + } + ] + ] + ]); + + $MutationType = new ObjectType([ + 'name' => 'Mutation', + 'fields' => [ + 'favoriteEnum' => [ + 'type' => $ColorType, + 'args' => ['color' => ['type' => $ColorType]], + 'resolve' => function ($value, $args) { + return isset($args['color']) ? $args['color'] : null; + } + ] + ] + ]); + + $SubscriptionType = new ObjectType([ + 'name' => 'Subscription', + 'fields' => [ + 'subscribeToEnum' => [ + 'type' => $ColorType, + 'args' => ['color' => ['type' => $ColorType]], + 'resolve' => function ($value, $args) { + return isset($args['color']) ? $args['color'] : null; + } + ] + ] + ]); + + $this->schema = new Schema([ + 'query' => $QueryType, + 'mutation' => $MutationType, + 'subscription' => $SubscriptionType + ]); + } + + // Describe: Type System: Enum Values + + /** + * @it accepts enum literals as input + */ + public function testAcceptsEnumLiteralsAsInput() + { + $this->assertEquals( + ['data' => ['colorInt' => 1]], + GraphQL::execute($this->schema, '{ colorInt(fromEnum: GREEN) }') + ); + } + + /** + * @it enum may be output type + */ + public function testEnumMayBeOutputType() + { + $this->assertEquals( + ['data' => ['colorEnum' => 'GREEN']], + GraphQL::execute($this->schema, '{ colorEnum(fromInt: 1) }') + ); + } + + /** + * @it enum may be both input and output type + */ + public function testEnumMayBeBothInputAndOutputType() + { + $this->assertEquals( + ['data' => ['colorEnum' => 'GREEN']], + GraphQL::execute($this->schema, '{ colorEnum(fromEnum: GREEN) }') + ); + } + + /** + * @it does not accept string literals + */ + public function testDoesNotAcceptStringLiterals() + { + $this->expectFailure( + '{ colorEnum(fromEnum: "GREEN") }', + null, + "Argument \"fromEnum\" has invalid value \"GREEN\".\nExpected type \"Color\", found \"GREEN\"." + ); + } + + /** + * @it does not accept incorrect internal value + */ + public function testDoesNotAcceptIncorrectInternalValue() + { + $this->assertEquals( + ['data' => ['colorEnum' => null]], + GraphQL::execute($this->schema, '{ colorEnum(fromString: "GREEN") }') + ); + } + + /** + * @it does not accept internal value in place of enum literal + */ + public function testDoesNotAcceptInternalValueInPlaceOfEnumLiteral() + { + $this->expectFailure( + '{ colorEnum(fromEnum: 1) }', + null, + "Argument \"fromEnum\" has invalid value 1.\nExpected type \"Color\", found 1." + ); + } + + /** + * @it does not accept enum literal in place of int + */ + public function testDoesNotAcceptEnumLiteralInPlaceOfInt() + { + $this->expectFailure( + '{ colorEnum(fromInt: GREEN) }', + null, + "Argument \"fromInt\" has invalid value GREEN.\nExpected type \"Int\", found GREEN." + ); + } + + /** + * @it accepts JSON string as enum variable + */ + public function testAcceptsJSONStringAsEnumVariable() + { + $this->assertEquals( + ['data' => ['colorEnum' => 'BLUE']], + GraphQL::execute( + $this->schema, + 'query test($color: Color!) { colorEnum(fromEnum: $color) }', + null, + ['color' => 'BLUE'] + ) + ); + } + + /** + * @it accepts enum literals as input arguments to mutations + */ + public function testAcceptsEnumLiteralsAsInputArgumentsToMutations() + { + $this->assertEquals( + ['data' => ['favoriteEnum' => 'GREEN']], + GraphQL::execute( + $this->schema, + 'mutation x($color: Color!) { favoriteEnum(color: $color) }', + null, + ['color' => 'GREEN'] + ) + ); + } + + /** + * @it accepts enum literals as input arguments to subscriptions + * @todo + */ + public function testAcceptsEnumLiteralsAsInputArgumentsToSubscriptions() + { + $this->markTestIncomplete('Enable when subscription support is implemented'); + + $this->assertEquals( + ['data' => ['subscribeToEnum' => 'GREEN'], 'errors' => [[]]], + GraphQL::execute( + $this->schema, + 'subscription x($color: Color!) { subscribeToEnum(color: $color) }', + null, + ['color' => 'GREEN'] + ) + ); + } + + /** + * @it does not accept internal value as enum variable + */ + public function testDoesNotAcceptInternalValueAsEnumVariable() + { + $this->expectFailure( + 'query test($color: Color!) { colorEnum(fromEnum: $color) }', + ['color' => 2], + "Variable \"\$color\" got invalid value 2.\nExpected type \"Color\", found 2." + ); + } + + /** + * @it does not accept string variables as enum input + */ + public function testDoesNotAcceptStringVariablesAsEnumInput() + { + $this->expectFailure( + 'query test($color: String!) { colorEnum(fromEnum: $color) }', + ['color' => 'BLUE'], + 'Variable "$color" of type "String!" used in position expecting type "Color".' + ); + } + + /** + * @it does not accept internal value variable as enum input + */ + public function testDoesNotAcceptInternalValueVariableSsEnumInput() + { + $this->expectFailure( + 'query test($color: Int!) { colorEnum(fromEnum: $color) }', + ['color' => 2], + 'Variable "$color" of type "Int!" used in position ' . 'expecting type "Color".' + ); + } + + /** + * @it enum value may have an internal value of 0 + */ + public function testEnumValueMayHaveAnInternalValueOf0() + { + $this->assertEquals( + ['data' => ['colorEnum' => 'RED', 'colorInt' => 0]], + GraphQL::execute($this->schema, "{ + colorEnum(fromEnum: RED) + colorInt(fromEnum: RED) + }") + ); + } + + /** + * @it enum inputs may be nullable + */ + public function testEnumInputsMayBeNullable() + { + $this->assertEquals( + ['data' => ['colorEnum' => null, 'colorInt' => null]], + GraphQL::execute($this->schema, "{ + colorEnum + colorInt + }") + ); + } + + private function expectFailure($query, $vars, $err) + { + $result = GraphQL::executeAndReturnResult($this->schema, $query, null, $vars); + $this->assertEquals(1, count($result->errors)); + + $this->assertEquals( + $err, + $result->errors[0]->getMessage() + ); + } +} diff --git a/tests/Type/ScalarSerializationTest.php b/tests/Type/ScalarSerializationTest.php index 83af55d..21f0864 100644 --- a/tests/Type/ScalarSerializationTest.php +++ b/tests/Type/ScalarSerializationTest.php @@ -5,38 +5,57 @@ use GraphQL\Type\Definition\Type; class ScalarSerializationTest extends \PHPUnit_Framework_TestCase { - public function testCoercesOutputInt() + // Type System: Scalar coercion + + /** + * @it serializes output int + */ + public function testSerializesOutputInt() { $intType = Type::int(); $this->assertSame(1, $intType->serialize(1)); $this->assertSame(0, $intType->serialize(0)); + $this->assertSame(-1, $intType->serialize(-1)); $this->assertSame(0, $intType->serialize(0.1)); $this->assertSame(1, $intType->serialize(1.1)); $this->assertSame(-1, $intType->serialize(-1.1)); $this->assertSame(100000, $intType->serialize(1e5)); + // Maybe a safe PHP int, but bigger than 2^32, so not + // representable as a GraphQL Int + $this->assertSame(null, $intType->serialize(9876504321)); + $this->assertSame(null, $intType->serialize(-9876504321)); $this->assertSame(null, $intType->serialize(1e100)); $this->assertSame(null, $intType->serialize(-1e100)); $this->assertSame(-1, $intType->serialize('-1.1')); $this->assertSame(null, $intType->serialize('one')); $this->assertSame(0, $intType->serialize(false)); + $this->assertSame(1, $intType->serialize(true)); } - public function testCoercesOutputFloat() + /** + * @it serializes output float + */ + public function testSerializesOutputFloat() { $floatType = Type::float(); $this->assertSame(1.0, $floatType->serialize(1)); + $this->assertSame(0.0, $floatType->serialize(0)); $this->assertSame(-1.0, $floatType->serialize(-1)); $this->assertSame(0.1, $floatType->serialize(0.1)); $this->assertSame(1.1, $floatType->serialize(1.1)); $this->assertSame(-1.1, $floatType->serialize(-1.1)); + $this->assertSame(-1.1, $floatType->serialize('-1.1')); $this->assertSame(null, $floatType->serialize('one')); $this->assertSame(0.0, $floatType->serialize(false)); $this->assertSame(1.0, $floatType->serialize(true)); } - public function testCoercesOutputStrings() + /** + * @it serializes output strings + */ + public function testSerializesOutputStrings() { $stringType = Type::string(); @@ -47,12 +66,16 @@ class ScalarSerializationTest extends \PHPUnit_Framework_TestCase $this->assertSame('false', $stringType->serialize(false)); } - public function testCoercesOutputBoolean() + /** + * @it serializes output boolean + */ + public function testSerializesOutputBoolean() { $boolType = Type::boolean(); $this->assertSame(true, $boolType->serialize('string')); $this->assertSame(false, $boolType->serialize('')); + $this->assertSame(true, $boolType->serialize('1')); $this->assertSame(true, $boolType->serialize(1)); $this->assertSame(false, $boolType->serialize(0)); $this->assertSame(true, $boolType->serialize(true));