From 841d6ab851e2d208f44a732af8d2116073a73ea8 Mon Sep 17 00:00:00 2001 From: vladar Date: Mon, 17 Aug 2015 20:01:55 +0600 Subject: [PATCH] Updated to latest version of graphql-js --- src/Executor/ExecutionContext.php | 11 +- src/Executor/ExecutionResult.php | 38 + src/Executor/Executor.php | 360 +++++---- src/Executor/Values.php | 245 +++--- src/GraphQL.php | 18 +- src/Language/AST/Argument.php | 7 +- src/Language/AST/Field.php | 7 +- src/Language/AST/FragmentDefinition.php | 7 +- src/Language/AST/FragmentSpread.php | 7 +- src/Language/AST/NamedType.php | 2 +- src/Language/AST/ObjectField.php | 7 +- src/Language/AST/OperationDefinition.php | 7 +- src/Language/AST/Variable.php | 7 +- src/Schema.php | 130 ++- src/Type/Definition/AbstractType.php | 11 + src/Type/Definition/BooleanType.php | 9 +- src/Type/Definition/Directive.php | 44 +- src/Type/Definition/EnumType.php | 4 +- src/Type/Definition/FieldArgument.php | 6 + src/Type/Definition/FieldDefinition.php | 18 + src/Type/Definition/FloatType.php | 14 +- src/Type/Definition/IDType.php | 9 +- src/Type/Definition/InputObjectType.php | 2 +- src/Type/Definition/IntType.php | 15 +- src/Type/Definition/InterfaceType.php | 18 +- src/Type/Definition/ObjectType.php | 69 +- src/Type/Definition/ResolveInfo.php | 61 ++ src/Type/Definition/ScalarType.php | 8 +- src/Type/Definition/ScalarTypeConfig.php | 26 - src/Type/Definition/StringType.php | 9 +- src/Type/Definition/Type.php | 26 +- src/Type/Definition/UnionType.php | 4 +- src/Type/Introspection.php | 194 ++++- src/Type/SchemaValidator.php | 7 +- src/Utils/TypeInfo.php | 72 +- src/Validator/DocumentValidator.php | 58 +- .../Rules/ArgumentsOfCorrectType.php | 54 +- .../Rules/FragmentsOnCompositeTypes.php | 28 +- src/Validator/Rules/KnownArgumentNames.php | 62 +- src/Validator/Rules/KnownDirectives.php | 18 +- src/Validator/Rules/KnownFragmentNames.php | 7 +- src/Validator/Rules/KnownTypeNames.php | 13 +- src/Validator/Rules/NoFragmentCycles.php | 9 +- src/Validator/Rules/NoUndefinedVariables.php | 14 +- src/Validator/Rules/NoUnusedFragments.php | 20 +- src/Validator/Rules/NoUnusedVariables.php | 7 +- .../Rules/OverlappingFieldsCanBeMerged.php | 104 ++- .../Rules/PossibleFragmentSpreads.php | 21 +- .../Rules/ProvidedNonNullArguments.php | 87 ++ src/Validator/Rules/ScalarLeafs.php | 14 +- .../Rules/VariablesAreInputTypes.php | 27 +- src/Validator/ValidationContext.php | 10 + tests/Executor/AbstractTest.php | 244 ++++++ tests/Executor/DirectivesTest.php | 38 +- tests/Executor/ExecutorSchemaTest.php | 2 +- tests/Executor/ExecutorTest.php | 168 +++- tests/Executor/ListsTest.php | 33 +- tests/Executor/MutationsTest.php | 12 +- tests/Executor/NonNullTest.php | 30 +- tests/Executor/TestClasses.php | 78 ++ tests/Executor/UnionInterfaceTest.php | 79 +- ...{InputObjectTest.php => VariablesTest.php} | 342 ++++---- tests/StarWarsSchema.php | 2 +- tests/StarWarsValidationTest.php | 25 +- tests/Type/DefinitionTest.php | 77 +- tests/Type/IntrospectionTest.php | 762 ++++++++---------- tests/Type/ScalarCoercionTest.php | 63 -- tests/Type/ScalarSerializationTest.php | 63 ++ tests/Type/SchemaValidatorTest.php | 93 +-- .../Validator/ArgumentsOfCorrectTypeTest.php | 34 +- .../DefaultValuesOfCorrectTypeTest.php | 4 +- tests/Validator/FieldsOnCorrectTypeTest.php | 11 +- .../FragmentsOnCompositeTypesTest.php | 8 +- tests/Validator/KnownArgumentNamesTest.php | 59 +- tests/Validator/KnownDirectivesTest.php | 36 +- tests/Validator/KnownFragmentNamesTest.php | 4 +- tests/Validator/KnownTypeNamesTest.php | 4 +- tests/Validator/NoFragmentCyclesTest.php | 34 +- tests/Validator/NoUndefinedVariablesTest.php | 8 +- tests/Validator/NoUnusedFragmentsTest.php | 21 +- tests/Validator/NoUnusedVariablesTest.php | 4 +- .../OverlappingFieldsCanBeMergedTest.php | 171 +++- .../Validator/PossibleFragmentSpreadsTest.php | 8 +- .../ProvidedNonNullArgumentsTest.php | 217 +++++ tests/Validator/ScalarLeafsTest.php | 16 +- tests/Validator/TestCase.php | 74 +- .../Validator/VariablesAreInputTypesTest.php | 14 +- .../VariablesInAllowedPositionTest.php | 26 +- 88 files changed, 3227 insertions(+), 1669 deletions(-) create mode 100644 src/Executor/ExecutionResult.php create mode 100644 src/Type/Definition/ResolveInfo.php delete mode 100644 src/Type/Definition/ScalarTypeConfig.php create mode 100644 src/Validator/Rules/ProvidedNonNullArguments.php create mode 100644 tests/Executor/AbstractTest.php create mode 100644 tests/Executor/TestClasses.php rename tests/Executor/{InputObjectTest.php => VariablesTest.php} (59%) delete mode 100644 tests/Type/ScalarCoercionTest.php create mode 100644 tests/Type/ScalarSerializationTest.php create mode 100644 tests/Validator/ProvidedNonNullArgumentsTest.php diff --git a/src/Executor/ExecutionContext.php b/src/Executor/ExecutionContext.php index 59b6454..8acf3db 100644 --- a/src/Executor/ExecutionContext.php +++ b/src/Executor/ExecutionContext.php @@ -1,6 +1,7 @@ schema = $schema; $this->fragments = $fragments; - $this->root = $root; + $this->rootValue = $root; $this->operation = $operation; - $this->variables = $variables; + $this->variableValues = $variables; $this->errors = $errors ?: []; } - public function addError($error) + public function addError(Error $error) { $this->errors[] = $error; return $this; diff --git a/src/Executor/ExecutionResult.php b/src/Executor/ExecutionResult.php new file mode 100644 index 0000000..f1b4487 --- /dev/null +++ b/src/Executor/ExecutionResult.php @@ -0,0 +1,38 @@ +data = $data; + $this->errors = $errors; + } + + public function toArray() + { + $result = ['data' => $this->data]; + + if (!empty($this->errors)) { + $result['errors'] = array_map(['GraphQL\Error', 'formatError'], $this->errors); + } + + return $result; + } +} diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 284ffd4..983e720 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\Node; use GraphQL\Language\AST\OperationDefinition; use GraphQL\Language\AST\SelectionSet; use GraphQL\Schema; +use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\FieldDefinition; @@ -16,6 +17,7 @@ use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; @@ -45,40 +47,70 @@ class Executor { private static $UNDEFINED; - public static function execute(Schema $schema, $root, Document $ast, $operationName = null, array $args = null) + private static $defaultResolveFn; + + /** + * Custom default resolve function + * + * @param $fn + * @throws \Exception + */ + public function setDefaultResolveFn($fn) + { + Utils::invariant(is_callable($fn), 'Expecting callable, but got ' . Utils::getVariableType($fn)); + self::$defaultResolveFn = $fn; + } + + /** + * @param Schema $schema + * @param Document $ast + * @param $rootValue + * @param array|\ArrayAccess $variableValues + * @param null $operationName + * @return ExecutionResult + */ + public static function execute(Schema $schema, Document $ast, $rootValue = null, $variableValues = null, $operationName = null) { if (!self::$UNDEFINED) { self::$UNDEFINED = new \stdClass(); } + if (null !== $variableValues) { + Utils::invariant( + is_array($variableValues) || $variableValues instanceof \ArrayAccess, + "Variable values are expected to be array or instance of ArrayAccess, got " . Utils::getVariableType($variableValues) + ); + } + if (null !== $operationName) { + Utils::invariant( + is_string($operationName), + "Operation name is supposed to be string, got " . Utils::getVariableType($operationName) + ); + } + + $exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $variableValues, $operationName); + try { - $errors = new \ArrayObject(); - $exeContext = self::buildExecutionContext($schema, $root, $ast, $operationName, $args, $errors); - $data = self::executeOperation($exeContext, $root, $exeContext->operation); - } catch (\Exception $e) { - $errors[] = $e; + $data = self::executeOperation($exeContext, $exeContext->operation, $rootValue); + } catch (Error $e) { + $exeContext->addError($e); + $data = null; } - $result = [ - 'data' => isset($data) ? $data : null - ]; - if (count($errors) > 0) { - $result['errors'] = array_map(['GraphQL\Error', 'formatError'], $errors->getArrayCopy()); - } - - return $result; + return new ExecutionResult($data, $exeContext->errors); } /** * Constructs a ExecutionContext object from the arguments passed to * execute, which we will pass throughout the other execution methods. */ - private static function buildExecutionContext(Schema $schema, $root, Document $ast, $operationName = null, array $args = null, &$errors) + private static function buildExecutionContext(Schema $schema, Document $documentAst, $rootValue, $rawVariableValues, $operationName = null) { + $errors = []; $operations = []; $fragments = []; - foreach ($ast->definitions as $statement) { + foreach ($documentAst->definitions as $statement) { switch ($statement->kind) { case Node::OPERATION_DEFINITION: $operations[$statement->name ? $statement->name->value : ''] = $statement; @@ -91,32 +123,34 @@ class Executor if (!$operationName && count($operations) !== 1) { throw new Error( - 'Must provide operation name if query contains multiple operations' + 'Must provide operation name if query contains multiple operations.' ); } $opName = $operationName ?: key($operations); - if (!isset($operations[$opName])) { - throw new Error('Unknown operation name: ' . $opName); + if (empty($operations[$opName])) { + throw new Error('Unknown operation named ' . $opName); } $operation = $operations[$opName]; - $variables = Values::getVariableValues($schema, $operation->variableDefinitions ?: array(), $args ?: []); - $exeContext = new ExecutionContext($schema, $fragments, $root, $operation, $variables, $errors); + $variableValues = Values::getVariableValues($schema, $operation->variableDefinitions ?: [], $rawVariableValues ?: []); + $exeContext = new ExecutionContext($schema, $fragments, $rootValue, $operation, $variableValues, $errors); return $exeContext; } /** * Implements the "Evaluating operations" section of the spec. */ - private static function executeOperation(ExecutionContext $exeContext, $root, OperationDefinition $operation) + private static function executeOperation(ExecutionContext $exeContext, OperationDefinition $operation, $rootValue) { $type = self::getOperationRootType($exeContext->schema, $operation); $fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject()); + if ($operation->operation === 'mutation') { - return self::executeFieldsSerially($exeContext, $type, $root, $fields->getArrayCopy()); + return self::executeFieldsSerially($exeContext, $type, $rootValue, $fields->getArrayCopy()); } - return self::executeFields($exeContext, $type, $root, $fields); + + return self::executeFields($exeContext, $type, $rootValue, $fields); } @@ -154,11 +188,11 @@ class Executor * Implements the "Evaluating selection sets" section of the spec * for "write" mode. */ - private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $source, $fields) + private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceValue, $fields) { $results = []; foreach ($fields as $responseName => $fieldASTs) { - $result = self::resolveField($exeContext, $parentType, $source, $fieldASTs); + $result = self::resolveField($exeContext, $parentType, $sourceValue, $fieldASTs); if ($result !== self::$UNDEFINED) { // Undefined means that field is not defined in schema @@ -250,18 +284,36 @@ class Executor } /** - * Determines if a field should be included based on @if and @unless directives. + * Determines if a field should be included based on the @include and @skip + * directives, where @skip has higher precedence than @include. */ private static function shouldIncludeNode(ExecutionContext $exeContext, $directives) { - $ifDirective = Values::getDirectiveValue(Directive::ifDirective(), $directives, $exeContext->variables); - if ($ifDirective !== null) { - return $ifDirective; + $skipDirective = Directive::skipDirective(); + $includeDirective = Directive::includeDirective(); + + /** @var \GraphQL\Language\AST\Directive $skipAST */ + $skipAST = $directives + ? Utils::find($directives, function(\GraphQL\Language\AST\Directive $directive) use ($skipDirective) { + return $directive->name->value === $skipDirective->name; + }) + : null; + + if ($skipAST) { + $argValues = Values::getArgumentValues($skipDirective->args, $skipAST->arguments, $exeContext->variableValues); + return empty($argValues['if']); } - $unlessDirective = Values::getDirectiveValue(Directive::unlessDirective(), $directives, $exeContext->variables); - if ($unlessDirective !== null) { - return !$unlessDirective; + /** @var \GraphQL\Language\AST\Directive $includeAST */ + $includeAST = $directives + ? Utils::find($directives, function(\GraphQL\Language\AST\Directive $directive) use ($includeDirective) { + return $directive->name->value === $includeDirective->name; + }) + : null; + + if ($includeAST) { + $argValues = Values::getArgumentValues($includeDirective->args, $includeAST->arguments, $exeContext->variableValues); + return !empty($argValues['if']); } return true; @@ -293,97 +345,106 @@ class Executor } /** - * A wrapper function for resolving the field, that catches the error - * and adds it to the context's global if the error is not rethrowable. + * Resolves the field on the given source object. In particular, this + * figures out the value that the field returns by calling its resolve function, + * then calls completeValue to complete promises, serialize scalars, or execute + * the sub-selection-set for objects. */ private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldASTs) { - $fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldASTs[0]); + $fieldAST = $fieldASTs[0]; + $fieldName = $fieldAST->name->value; + + $fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName); + if (!$fieldDef) { return self::$UNDEFINED; } + $returnType = $fieldDef->getType(); + + if (isset($fieldDef->resolve)) { + $resolveFn = $fieldDef->resolve; + } else if (isset(self::$defaultResolveFn)) { + $resolveFn = self::$defaultResolveFn; + } else { + $resolveFn = [__CLASS__, 'defaultResolveFn']; + } + + // Build hash of arguments from the field.arguments AST, using the + // variables scope to fulfill any variable references. + // TODO: find a way to memoize, in case this field is within a List type. + $args = Values::getArgumentValues( + $fieldDef->args, + $fieldAST->arguments, + $exeContext->variableValues + ); + + // The resolve function's optional third argument is a collection of + // information about the current execution state. + $info = new ResolveInfo([ + 'fieldName' => $fieldName, + 'fieldASTs' => $fieldASTs, + 'returnType' => $returnType, + 'parentType' => $parentType, + 'schema' => $exeContext->schema, + 'fragments' => $exeContext->fragments, + 'rootValue' => $exeContext->rootValue, + 'operation' => $exeContext->operation, + 'variableValues' => $exeContext->variableValues, + ]); + + // If an error occurs while calling the field `resolve` function, ensure that + // it is wrapped as a GraphQLError with locations. Log this error and return + // null if allowed, otherwise throw the error so the parent field can handle + // it. + try { + $result = call_user_func($resolveFn, $source, $args, $info); + } catch (\Exception $error) { + $reportedError = Error::createLocatedError($error, $fieldASTs); + + if ($returnType instanceof NonNull) { + throw $reportedError; + } + + $exeContext->addError($reportedError); + return null; + } + + return self::completeValueCatchingError( + $exeContext, + $returnType, + $fieldASTs, + $info, + $result + ); + } + + + public static function completeValueCatchingError( + ExecutionContext $exeContext, + Type $returnType, + $fieldASTs, + ResolveInfo $info, + $result + ) + { // If the field type is non-nullable, then it is resolved without any // protection from errors. - if ($fieldDef->getType() instanceof NonNull) { - return self::resolveFieldOrError( - $exeContext, - $parentType, - $source, - $fieldASTs, - $fieldDef - ); + if ($returnType instanceof NonNull) { + return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result); } // Otherwise, error protection is applied, logging the error and resolving // a null value for this field if one is encountered. try { - $result = self::resolveFieldOrError( - $exeContext, - $parentType, - $source, - $fieldASTs, - $fieldDef - ); - - return $result; - } catch (\Exception $error) { - $exeContext->addError($error); + return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result); + } catch (Error $err) { + $exeContext->addError($err); return null; } } - /** - * Resolves the field on the given source object. In particular, this - * figures out the object that the field returns using the resolve function, - * then calls completeField to coerce scalars or execute the sub - * selection set for objects. - */ - private static function resolveFieldOrError( - ExecutionContext $exeContext, - ObjectType $parentType, - $source, - /*array*/ $fieldASTs, - FieldDefinition $fieldDef - ) - { - $fieldAST = $fieldASTs[0]; - $fieldType = $fieldDef->getType(); - $resolveFn = $fieldDef->resolve ?: [__CLASS__, 'defaultResolveFn']; - - // Build a JS object of arguments from the field.arguments AST, using the - // variables scope to fulfill any variable references. - // TODO: find a way to memoize, in case this field is within a Array type. - $args = Values::getArgumentValues( - $fieldDef->args, - $fieldAST->arguments, - $exeContext->variables - ); - - try { - $result = call_user_func($resolveFn, - $source, - $args, - $exeContext->root, - // TODO: provide all fieldASTs, not just the first field - $fieldAST, - $fieldType, - $parentType, - $exeContext->schema - ); - } catch (\Exception $error) { - throw Error::createLocatedError($error, [$fieldAST]); - } - - return self::completeField( - $exeContext, - $fieldType, - $fieldASTs, - $result - ); - } - - /** * Implements the instructions for completeValue as defined in the * "Field entries" section of the spec. @@ -396,20 +457,22 @@ class Executor * for the inner type on each item in the list. * * If the field type is a Scalar or Enum, ensures the completed value is a legal - * value of the type by calling the `coerce` method of GraphQL type definition. + * value of the type by calling the `serialize` method of GraphQL type + * definition. * * Otherwise, the field type expects a sub-selection set, and will complete the * value by evaluating all sub-selections. */ - private static function completeField(ExecutionContext $exeContext, Type $fieldType,/* Array */ $fieldASTs, &$result) + private static function completeValue(ExecutionContext $exeContext, Type $returnType,/* Array */ $fieldASTs, ResolveInfo $info, &$result) { // If field type is NonNull, complete for inner type, and throw field error // if result is null. - if ($fieldType instanceof NonNull) { - $completed = self::completeField( + if ($returnType instanceof NonNull) { + $completed = self::completeValue( $exeContext, - $fieldType->getWrappedType(), + $returnType->getWrappedType(), $fieldASTs, + $info, $result ); if ($completed === null) { @@ -427,8 +490,8 @@ class Executor } // If field type is List, complete each item in the list with the inner type - if ($fieldType instanceof ListOfType) { - $itemType = $fieldType->getWrappedType(); + if ($returnType instanceof ListOfType) { + $itemType = $returnType->getWrappedType(); Utils::invariant( is_array($result) || $result instanceof \Traversable, 'User Error: expected iterable, but did not find one.' @@ -436,32 +499,48 @@ class Executor $tmp = []; foreach ($result as $item) { - $tmp[] = self::completeField($exeContext, $itemType, $fieldASTs, $item); + $tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item); } return $tmp; } - // If field type is Scalar or Enum, coerce to a valid value, returning null - // if coercion is not possible. - if ($fieldType instanceof ScalarType || - $fieldType instanceof EnumType - ) { - Utils::invariant(method_exists($fieldType, 'coerce'), 'Missing coerce method on type'); - return $fieldType->coerce($result); + // If field type is Scalar or Enum, serialize to a valid value, returning + // null if serialization is not possible. + if ($returnType instanceof ScalarType || + $returnType instanceof EnumType) { + Utils::invariant(method_exists($returnType, 'serialize'), 'Missing serialize method on type'); + return $returnType->serialize($result); } // Field type must be Object, Interface or Union and expect sub-selections. + if ($returnType instanceof ObjectType) { + $objectType = $returnType; + } else if ($returnType instanceof AbstractType) { + $objectType = $returnType->getObjectType($result, $info); - $objectType = - $fieldType instanceof ObjectType ? $fieldType : - ($fieldType instanceof InterfaceType || - $fieldType instanceof UnionType ? $fieldType->resolveType($result) : - null); + if ($objectType && !$returnType->isPossibleType($objectType)) { + throw new Error( + "Runtime Object type \"$objectType\" is not a possible type for \"$returnType\"." + ); + } + } else { + $objectType = null; + } if (!$objectType) { return null; } + // If there is an isTypeOf predicate function, call it with the + // current result. If isTypeOf returns false, then raise an error rather + // than continuing execution. + if (false === $objectType->isTypeOf($result, $info)) { + throw new Error( + "Expected value of type $objectType but got: $result.", + $fieldASTs + ); + } + // Collect sub-fields to execute to complete this value. $subFieldASTs = new \ArrayObject(); $visitedFragmentNames = new \ArrayObject(); @@ -488,17 +567,18 @@ class Executor * and returns it as the result, or if it's a function, returns the result * of calling that function. */ - public static function defaultResolveFn($source, $args, $root, $fieldAST) + public static function defaultResolveFn($source, $args, ResolveInfo $info) { + $fieldName = $info->fieldName; $property = null; + if (is_array($source) || $source instanceof \ArrayAccess) { - if (isset($source[$fieldAST->name->value])) { - $property = $source[$fieldAST->name->value]; + if (isset($source[$fieldName])) { + $property = $source[$fieldName]; } } else if (is_object($source)) { - if (property_exists($source, $fieldAST->name->value)) { - $e = func_get_args(); - $property = $source->{$fieldAST->name->value}; + if (property_exists($source, $fieldName)) { + $property = $source->{$fieldName}; } } @@ -516,25 +596,21 @@ class Executor * * @return FieldDefinition */ - private static function getFieldDef(Schema $schema, ObjectType $parentType, Field $fieldAST) + private static function getFieldDef(Schema $schema, ObjectType $parentType, $fieldName) { - $name = $fieldAST->name->value; $schemaMetaFieldDef = Introspection::schemaMetaFieldDef(); $typeMetaFieldDef = Introspection::typeMetaFieldDef(); $typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef(); - if ($name === $schemaMetaFieldDef->name && - $schema->getQueryType() === $parentType - ) { + if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) { return $schemaMetaFieldDef; - } else if ($name === $typeMetaFieldDef->name && - $schema->getQueryType() === $parentType - ) { + } else if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) { return $typeMetaFieldDef; - } else if ($name === $typeNameMetaFieldDef->name) { + } else if ($fieldName === $typeNameMetaFieldDef->name) { return $typeNameMetaFieldDef; } + $tmp = $parentType->getFields(); - return isset($tmp[$name]) ? $tmp[$name] : null; + return isset($tmp[$fieldName]) ? $tmp[$fieldName] : null; } } diff --git a/src/Executor/Values.php b/src/Executor/Values.php index 3636eb7..5d0caaf 100644 --- a/src/Executor/Values.php +++ b/src/Executor/Values.php @@ -3,16 +3,25 @@ namespace GraphQL\Executor; use GraphQL\Error; +use GraphQL\Language\AST\Argument; +use GraphQL\Language\AST\ListType; +use GraphQL\Language\AST\ListValue; use GraphQL\Language\AST\Node; +use GraphQL\Language\AST\ObjectValue; +use GraphQL\Language\AST\Value; +use GraphQL\Language\AST\Variable; use GraphQL\Language\AST\VariableDefinition; use GraphQL\Language\Printer; use GraphQL\Schema; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; +use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InputObjectType; +use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; +use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Utils; @@ -23,8 +32,14 @@ class Values * Prepares an object map of variables of the correct type based on the provided * variable definitions and arbitrary input. If the input cannot be coerced * to match the variable definitions, a Error will be thrown. + * + * @param Schema $schema + * @param VariableDefinition[] $definitionASTs + * @param array $inputs + * @return array + * @throws Error */ - public static function getVariableValues(Schema $schema, /* Array */ $definitionASTs, array $inputs) + public static function getVariableValues(Schema $schema, $definitionASTs, array $inputs) { $values = []; foreach ($definitionASTs as $defAST) { @@ -37,11 +52,16 @@ class Values /** * Prepares an object map of argument values given a list of argument * definitions and list of argument AST nodes. + * + * @param FieldArgument[] $argDefs + * @param Argument[] $argASTs + * @param $variableValues + * @return array */ - public static function getArgumentValues(/* Array*/ $argDefs, /*Array*/ $argASTs, $variables) + public static function getArgumentValues($argDefs, $argASTs, $variableValues) { - if (!$argDefs || count($argDefs) === 0) { - return null; + if (!$argDefs || !$argASTs) { + return []; } $argASTMap = $argASTs ? Utils::keyMap($argASTs, function ($arg) { return $arg->name->value; @@ -50,28 +70,71 @@ class Values foreach ($argDefs as $argDef) { $name = $argDef->name; $valueAST = isset($argASTMap[$name]) ? $argASTMap[$name]->value : null; - $result[$name] = self::coerceValueAST($argDef->getType(), $valueAST, $variables); + $value = self::valueFromAST($valueAST, $argDef->getType(), $variableValues); + + if (null === $value) { + $value = $argDef->defaultValue; + } + if (null !== $value) { + $result[$name] = $value; + } } return $result; } - public static function getDirectiveValue(Directive $directiveDef, /* Array */ $directives, $variables) + public static function valueFromAST($valueAST, InputType $type, $variables = null) { - $directiveAST = null; - if ($directives) { - foreach ($directives as $directive) { - if ($directive->name->value === $directiveDef->name) { - $directiveAST = $directive; - break; - } - } + if ($type instanceof NonNull) { + return self::valueFromAST($valueAST, $type->getWrappedType(), $variables); } - if ($directiveAST) { - if (!$directiveDef->type) { + + if (!$valueAST) { + return null; + } + + if ($valueAST instanceof Variable) { + $variableName = $valueAST->name->value; + + if (!$variables || !isset($variables[$variableName])) { return null; } - return self::coerceValueAST($directiveDef->type, $directiveAST->value, $variables); + return $variables[$variableName]; } + + if ($type instanceof ListOfType) { + $itemType = $type->getWrappedType(); + if ($valueAST instanceof ListValue) { + return array_map(function($itemAST) use ($itemType, $variables) { + return Values::valueFromAST($itemAST, $itemType, $variables); + }, $valueAST->values); + } else { + return [self::valueFromAST($valueAST, $itemType, $variables)]; + } + } + + if ($type instanceof InputObjectType) { + $fields = $type->getFields(); + if (!$valueAST instanceof ObjectValue) { + return null; + } + $fieldASTs = Utils::keyMap($valueAST->fields, function($field) {return $field->name->value;}); + $values = []; + foreach ($fields as $field) { + $fieldAST = isset($fieldASTs[$field->name]) ? $fieldASTs[$field->name] : null; + $fieldValue = self::valueFromAST($fieldAST ? $fieldAST->value : null, $field->getType(), $variables); + + if (null === $fieldValue) { + $fieldValue = $field->defaultValue; + } + if (null !== $fieldValue) { + $values[$field->name] = $fieldValue; + } + } + return $values; + } + + Utils::invariant($type instanceof ScalarType || $type instanceof EnumType, 'Must be input type'); + return $type->parseLiteral($valueAST); } /** @@ -81,14 +144,21 @@ class Values private static function getVariableValue(Schema $schema, VariableDefinition $definitionAST, $input) { $type = Utils\TypeInfo::typeFromAST($schema, $definitionAST->type); - if (!$type) { - return null; + $variable = $definitionAST->variable; + + if (!$type || !Type::isInputType($type)) { + $printed = Printer::doPrint($definitionAST->type); + throw new Error( + "Variable \"\${$variable->name->value}\" expected value of type " . + "\"$printed\" which cannot be used as an input type.", + [ $definitionAST ] + ); } - if (self::isValidValue($type, $input)) { + if (self::isValidValue($input, $type)) { if (null === $input) { $defaultValue = $definitionAST->defaultValue; if ($defaultValue) { - return self::coerceValueAST($type, $defaultValue); + return self::valueFromAST($defaultValue, $type); } } return self::coerceValue($type, $input); @@ -103,15 +173,21 @@ class Values /** - * Given a type and any value, return true if that value is valid. + * Given a PHP value and a GraphQL type, determine if the value will be + * accepted for that type. This is primarily useful for validating the + * runtime values of query variables. + * + * @param $value + * @param Type $type + * @return bool */ - private static function isValidValue(Type $type, $value) + private static function isValidValue($value, Type $type) { if ($type instanceof NonNull) { if (null === $value) { return false; } - return self::isValidValue($type->getWrappedType(), $value); + return self::isValidValue($value, $type->getWrappedType()); } if ($value === null) { @@ -122,34 +198,44 @@ class Values $itemType = $type->getWrappedType(); if (is_array($value)) { foreach ($value as $item) { - if (!self::isValidValue($itemType, $item)) { + if (!self::isValidValue($item, $itemType)) { return false; } } return true; } else { - return self::isValidValue($itemType, $value); + return self::isValidValue($value, $itemType); } } if ($type instanceof InputObjectType) { + if (!is_array($value)) { + return false; + } $fields = $type->getFields(); + $fieldMap = []; + + // Ensure every defined field is valid. foreach ($fields as $fieldName => $field) { /** @var FieldDefinition $field */ - if (!self::isValidValue($field->getType(), isset($value[$fieldName]) ? $value[$fieldName] : null)) { + if (!self::isValidValue(isset($value[$fieldName]) ? $value[$fieldName] : null, $field->getType())) { return false; } + $fieldMap[$field->name] = $field; } + + // Ensure every provided field is defined. + $diff = array_diff_key($value, $fieldMap); + + if (!empty($diff)) { + return false; + } + return true; } - if ($type instanceof ScalarType || - $type instanceof EnumType - ) { - return null !== $type->coerce($value); - } - - return false; + Utils::invariant($type instanceof ScalarType || $type instanceof EnumType, 'Must be input type'); + return null !== $type->parseValue($value); } /** @@ -183,93 +269,18 @@ class Values $fields = $type->getFields(); $obj = []; foreach ($fields as $fieldName => $field) { - $fieldValue = self::coerceValue($field->getType(), $value[$fieldName]); - $obj[$fieldName] = $fieldValue === null ? $field->defaultValue : $fieldValue; - } - return $obj; - - } - - if ($type instanceof ScalarType || - $type instanceof EnumType - ) { - $coerced = $type->coerce($value); - if (null !== $coerced) { - return $coerced; - } - } - - return null; - } - - /** - * Given a type and a value AST node known to match this type, build a - * runtime value. - */ - private static function coerceValueAST(Type $type, $valueAST, $variables) - { - if ($type instanceof NonNull) { - // Note: we're not checking that the result of coerceValueAST is non-null. - // We're assuming that this query has been validated and the value used - // here is of the correct type. - return self::coerceValueAST($type->getWrappedType(), $valueAST, $variables); - } - - if (!$valueAST) { - return null; - } - - if ($valueAST->kind === Node::VARIABLE) { - $variableName = $valueAST->name->value; - - if (!isset($variables, $variables[$variableName])) { - return null; - } - - // Note: we're not doing any checking that this variable is correct. We're - // assuming that this query has been validated and the variable usage here - // is of the correct type. - return $variables[$variableName]; - } - - if ($type instanceof ListOfType) { - $itemType = $type->getWrappedType(); - if ($valueAST->kind === Node::ARR) { - $tmp = []; - foreach ($valueAST->values as $itemAST) { - $tmp[] = self::coerceValueAST($itemType, $itemAST, $variables); + $fieldValue = self::coerceValue($field->getType(), isset($value[$fieldName]) ? $value[$fieldName] : null); + if (null === $fieldValue) { + $fieldValue = $field->defaultValue; + } + if (null !== $fieldValue) { + $obj[$fieldName] = $fieldValue; } - return $tmp; - } else { - return [self::coerceValueAST($itemType, $valueAST, $variables)]; - } - } - - if ($type instanceof InputObjectType) { - $fields = $type->getFields(); - if ($valueAST->kind !== Node::OBJECT) { - return null; - } - $fieldASTs = Utils::keyMap($valueAST->fields, function ($field) { - return $field->name->value; - }); - - $obj = []; - foreach ($fields as $fieldName => $field) { - $fieldAST = $fieldASTs[$fieldName]; - $fieldValue = self::coerceValueAST($field->getType(), $fieldAST ? $fieldAST->value : null, $variables); - $obj[$fieldName] = $fieldValue === null ? $field->defaultValue : $fieldValue; } return $obj; - } - if ($type instanceof ScalarType || $type instanceof EnumType) { - $coerced = $type->coerceLiteral($valueAST); - if (null !== $coerced) { - return $coerced; - } } - - return null; + Utils::invariant($type instanceof ScalarType || $type instanceof EnumType, 'Must be input type'); + return $type->parseValue($value); } } diff --git a/src/GraphQL.php b/src/GraphQL.php index f2c7cab..42811c3 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -11,25 +11,25 @@ class GraphQL /** * @param Schema $schema * @param $requestString - * @param mixed $rootObject + * @param mixed $rootValue * @param array |null $variableValues * @param string|null $operationName * @return array */ - public static function execute(Schema $schema, $requestString, $rootObject = null, $variableValues = null, $operationName = null) + public static function execute(Schema $schema, $requestString, $rootValue = null, $variableValues = null, $operationName = null) { try { $source = new Source($requestString ?: '', 'GraphQL request'); - $ast = Parser::parse($source); - $validationResult = DocumentValidator::validate($schema, $ast); + $documentAST = Parser::parse($source); + $validationErrors = DocumentValidator::validate($schema, $documentAST); - if (empty($validationResult['isValid'])) { - return ['errors' => $validationResult['errors']]; + if (!empty($validationErrors)) { + return ['errors' => array_map(['GraphQL\Error', 'formatError'], $validationErrors)]; } else { - return Executor::execute($schema, $rootObject, $ast, $operationName, $variableValues); + return Executor::execute($schema, $documentAST, $rootValue, $variableValues, $operationName)->toArray(); } - } catch (\Exception $e) { - return ['errors' => Error::formatError($e)]; + } catch (Error $e) { + return ['errors' => [Error::formatError($e)]]; } } } diff --git a/src/Language/AST/Argument.php b/src/Language/AST/Argument.php index 6d1bc9d..18579f0 100644 --- a/src/Language/AST/Argument.php +++ b/src/Language/AST/Argument.php @@ -1,7 +1,7 @@ */ diff --git a/src/Language/AST/NamedType.php b/src/Language/AST/NamedType.php index ce6a6aa..b524364 100644 --- a/src/Language/AST/NamedType.php +++ b/src/Language/AST/NamedType.php @@ -1,7 +1,7 @@ querySchema = $querySchema; $this->mutationSchema = $mutationSchema; + + // Build type map now to detect any errors within this schema. + $map = []; + foreach ([$this->getQueryType(), $this->getMutationType(), Introspection::_schema()] as $type) { + $this->_extractTypes($type, $map); + } + $this->_typeMap = $map + Type::getInternalTypes(); + + // Enforce correct interface implementations + foreach ($this->_typeMap as $typeName => $type) { + if ($type instanceof ObjectType) { + foreach ($type->getInterfaces() as $iface) { + $this->assertObjectImplementsInterface($type, $iface); + } + } + } + } + + /** + * @param ObjectType $object + * @param InterfaceType $iface + * @throws \Exception + */ + private function assertObjectImplementsInterface(ObjectType $object, InterfaceType $iface) + { + $objectFieldMap = $object->getFields(); + $ifaceFieldMap = $iface->getFields(); + + foreach ($ifaceFieldMap as $fieldName => $ifaceField) { + Utils::invariant( + isset($objectFieldMap[$fieldName]), + "\"$iface\" expects field \"$fieldName\" but \"$object\" does not provide it" + ); + + /** @var $ifaceField FieldDefinition */ + /** @var $objectField FieldDefinition */ + $objectField = $objectFieldMap[$fieldName]; + + Utils::invariant( + $this->isEqualType($ifaceField->getType(), $objectField->getType()), + "$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " . + "$object.$fieldName provides type \"{$objectField->getType()}" + ); + + foreach ($ifaceField->args as $ifaceArg) { + /** @var $ifaceArg FieldArgument */ + /** @var $objectArg FieldArgument */ + $argName = $ifaceArg->name; + $objectArg = $objectField->getArg($argName); + + // Assert interface field arg exists on object field. + Utils::invariant( + $objectArg, + "$iface.$fieldName expects argument \"$argName\" but $object.$fieldName does not provide it." + ); + + // Assert interface field arg type matches object field arg type. + // (invariant) + Utils::invariant( + $this->isEqualType($ifaceArg->getType(), $objectArg->getType()), + "$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " . + "but $object.$fieldName($argName:) provides " . + "type \"{$objectArg->getType()}\"" + ); + + // Assert argument set invariance. + foreach ($objectField->args as $objectArg) { + $argName = $objectArg->name; + $ifaceArg = $ifaceField->getArg($argName); + Utils::invariant( + $ifaceArg, + "$iface.$fieldName does not define argument \"$argName\" but " . + "$object.$fieldName provides it." + ); + } + } + } + } + + /** + * @param $typeA + * @param $typeB + * @return bool + */ + private function isEqualType($typeA, $typeB) + { + if ($typeA instanceof NonNull && $typeB instanceof NonNull) { + return $this->isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); + } + if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) { + return $this->isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); + } + return $typeA === $typeB; } public function getQueryType() @@ -57,8 +155,8 @@ class Schema { if (!$this->_directives) { $this->_directives = [ - Directive::ifDirective(), - Directive::unlessDirective() + Directive::includeDirective(), + Directive::skipDirective() ]; } return $this->_directives; @@ -66,27 +164,26 @@ class Schema public function getTypeMap() { - if (null === $this->_typeMap) { - $map = []; - foreach ([$this->getQueryType(), $this->getMutationType(), Introspection::_schema()] as $type) { - $this->_extractTypes($type, $map); - } - $this->_typeMap = $map + Type::getInternalTypes(); - } return $this->_typeMap; } private function _extractTypes($type, &$map) { + if (!$type) { + return $map; + } + if ($type instanceof WrappingType) { return $this->_extractTypes($type->getWrappedType(), $map); } - if (!$type instanceof Type || !empty($map[$type->name])) { - // TODO: warning? + if (!empty($map[$type->name])) { + Utils::invariant( + $map[$type->name] === $type, + "Schema must contain unique named types but contains multiple types named \"$type\"." + ); return $map; } - $map[$type->name] = $type; $nestedTypes = []; @@ -97,13 +194,12 @@ class Schema if ($type instanceof ObjectType) { $nestedTypes = array_merge($nestedTypes, $type->getInterfaces()); } - if ($type instanceof ObjectType || $type instanceof InterfaceType) { + if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) { foreach ((array) $type->getFields() as $fieldName => $field) { - if (null === $field->args) { - trigger_error('WTF ' . $field->name . ' has no args?'); // gg + if (isset($field->args)) { + $fieldArgTypes = array_map(function($arg) { return $arg->getType(); }, $field->args); + $nestedTypes = array_merge($nestedTypes, $fieldArgTypes); } - $fieldArgTypes = array_map(function($arg) { return $arg->getType(); }, $field->args); - $nestedTypes = array_merge($nestedTypes, $fieldArgTypes); $nestedTypes[] = $field->getType(); } } diff --git a/src/Type/Definition/AbstractType.php b/src/Type/Definition/AbstractType.php index af5a55b..453af24 100644 --- a/src/Type/Definition/AbstractType.php +++ b/src/Type/Definition/AbstractType.php @@ -13,4 +13,15 @@ GraphQLUnionType; * @return array */ public function getPossibleTypes(); + + /** + * @return ObjectType + */ + public function getObjectType($value, ResolveInfo $info); + + /** + * @param Type $type + * @return bool + */ + public function isPossibleType(Type $type); } diff --git a/src/Type/Definition/BooleanType.php b/src/Type/Definition/BooleanType.php index d3b2676..3446094 100644 --- a/src/Type/Definition/BooleanType.php +++ b/src/Type/Definition/BooleanType.php @@ -7,12 +7,17 @@ class BooleanType extends ScalarType { public $name = Type::BOOLEAN; - public function coerce($value) + public function serialize($value) { return !!$value; } - public function coerceLiteral($ast) + public function parseValue($value) + { + return !!$value; + } + + public function parseLiteral($ast) { if ($ast instanceof BooleanValue) { return (bool) $ast->value; diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index 9545b9b..abb9b41 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -8,39 +8,51 @@ class Directive /** * @return Directive */ - public static function ifDirective() + public static function includeDirective() { $internal = self::getInternalDirectives(); - return $internal['if']; + return $internal['include']; } /** * @return Directive */ - public static function unlessDirective() + public static function skipDirective() { $internal = self::getInternalDirectives(); - return $internal['unless']; + return $internal['skip']; } public static function getInternalDirectives() { if (!self::$internalDirectives) { self::$internalDirectives = [ - 'if' => new self([ - 'name' => 'if', - 'description' => 'Directs the executor to omit this field if the argument provided is false.', - 'type' => Type::nonNull(Type::boolean()), + 'include' => new self([ + 'name' => 'include', + 'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.', + 'args' => [ + new FieldArgument([ + 'name' => 'if', + 'type' => Type::nonNull(Type::boolean()), + 'description' => 'Included when true.' + ]) + ], 'onOperation' => false, - 'onFragment' => false, + 'onFragment' => true, 'onField' => true ]), - 'unless' => new self([ - 'name' => 'unless', - 'description' => 'Directs the executor to omit this field if the argument provided is true.', - 'type' => Type::nonNull(Type::boolean()), + 'skip' => new self([ + 'name' => 'skip', + 'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.', + 'args' => [ + new FieldArgument([ + 'name' => 'if', + 'type' => Type::nonNull(Type::boolean()), + 'description' => 'Skipped when true' + ]) + ], 'onOperation' => false, - 'onFragment' => false, + 'onFragment' => true, 'onField' => true ]) ]; @@ -59,9 +71,9 @@ class Directive public $description; /** - * @var Type + * @var FieldArgument[] */ - public $type; + public $args; /** * @var boolean diff --git a/src/Type/Definition/EnumType.php b/src/Type/Definition/EnumType.php index 0d7a8c8..0f58c28 100644 --- a/src/Type/Definition/EnumType.php +++ b/src/Type/Definition/EnumType.php @@ -53,13 +53,13 @@ class EnumType extends Type implements InputType, OutputType return $this->_values; } - public function coerce($value) + public function serialize($value) { $enumValue = $this->_getValueLookup()->offsetGet($value); return $enumValue ? $enumValue->name : null; } - public function coerceLiteral($value) + public function parseLiteral($value) { if ($value instanceof EnumValue) { $lookup = $this->_getNameLookup(); diff --git a/src/Type/Definition/FieldArgument.php b/src/Type/Definition/FieldArgument.php index 1acb3ac..c3e0e6a 100644 --- a/src/Type/Definition/FieldArgument.php +++ b/src/Type/Definition/FieldArgument.php @@ -4,6 +4,12 @@ namespace GraphQL\Type\Definition; use GraphQL\Utils; +/** + * Class FieldArgument + * + * @package GraphQL\Type\Definition + * @todo Rename to Argument as it is also applicable to directives, not only fields + */ class FieldArgument { /** diff --git a/src/Type/Definition/FieldDefinition.php b/src/Type/Definition/FieldDefinition.php index dd76eb6..b8d8cf3 100644 --- a/src/Type/Definition/FieldDefinition.php +++ b/src/Type/Definition/FieldDefinition.php @@ -1,6 +1,8 @@ deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null; } + /** + * @param $name + * @return FieldArgument|null + */ + public function getArg($name) + { + foreach ($this->args ?: [] as $arg) { + /** @var FieldArgument $arg */ + if ($arg->name === $name) { + return $arg; + } + } + return null; + } + /** * @return Type */ public function getType() { if (null === $this->resolvedType) { + // TODO: deprecate types as callbacks - instead just allow field definitions to be callbacks $this->resolvedType = Type::resolve($this->type); } return $this->resolvedType; diff --git a/src/Type/Definition/FloatType.php b/src/Type/Definition/FloatType.php index 30b204a..da58b47 100644 --- a/src/Type/Definition/FloatType.php +++ b/src/Type/Definition/FloatType.php @@ -8,12 +8,22 @@ class FloatType extends ScalarType { public $name = Type::FLOAT; - public function coerce($value) + public function serialize($value) + { + return $this->coerceFloat($value); + } + + public function parseValue($value) + { + return $this->coerceFloat($value); + } + + private function coerceFloat($value) { return is_numeric($value) || $value === true || $value === false ? (float) $value : null; } - public function coerceLiteral($ast) + public function parseLiteral($ast) { if ($ast instanceof FloatValue || $ast instanceof IntValue) { return (float) $ast->value; diff --git a/src/Type/Definition/IDType.php b/src/Type/Definition/IDType.php index d8947fb..e6f1deb 100644 --- a/src/Type/Definition/IDType.php +++ b/src/Type/Definition/IDType.php @@ -8,12 +8,17 @@ class IDType extends ScalarType { public $name = 'ID'; - public function coerce($value) + public function serialize($value) { return (string) $value; } - public function coerceLiteral($ast) + public function parseValue($value) + { + return (string) $value; + } + + public function parseLiteral($ast) { if ($ast instanceof StringValue || $ast instanceof IntValue) { return $ast->value; diff --git a/src/Type/Definition/InputObjectType.php b/src/Type/Definition/InputObjectType.php index e85d766..8230c72 100644 --- a/src/Type/Definition/InputObjectType.php +++ b/src/Type/Definition/InputObjectType.php @@ -32,7 +32,7 @@ class InputObjectType extends Type implements InputType } /** - * @return array + * @return InputObjectField[] */ public function getFields() { diff --git a/src/Type/Definition/IntType.php b/src/Type/Definition/IntType.php index 0c7a160..8370e53 100644 --- a/src/Type/Definition/IntType.php +++ b/src/Type/Definition/IntType.php @@ -2,12 +2,23 @@ namespace GraphQL\Type\Definition; use GraphQL\Language\AST\IntValue; +use GraphQL\Language\AST\Value; class IntType extends ScalarType { public $name = Type::INT; - public function coerce($value) + public function serialize($value) + { + return $this->coerceInt($value); + } + + public function parseValue($value) + { + return $this->coerceInt($value); + } + + private function coerceInt($value) { if (false === $value || true === $value) { return (int) $value; @@ -18,7 +29,7 @@ class IntType extends ScalarType return null; } - public function coerceLiteral($ast) + public function parseLiteral($ast) { if ($ast instanceof IntValue) { $val = (int) $ast->value; diff --git a/src/Type/Definition/InterfaceType.php b/src/Type/Definition/InterfaceType.php index 5f15396..f1d1650 100644 --- a/src/Type/Definition/InterfaceType.php +++ b/src/Type/Definition/InterfaceType.php @@ -35,11 +35,11 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT * implementation for Interface types. * * @param ObjectType $impl - * @param array $interfaces + * @param InterfaceType[] $interfaces */ - public static function addImplementationToInterfaces(ObjectType $impl, array $interfaces) + public static function addImplementationToInterfaces(ObjectType $impl) { - foreach ($interfaces as $interface) { + foreach ($impl->getInterfaces() as $interface) { $interface->_implementations[] = $impl; } } @@ -84,10 +84,10 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT return $this->_implementations; } - public function isPossibleType(ObjectType $type) + public function isPossibleType(Type $type) { $possibleTypeNames = $this->_possibleTypeNames; - if (!$possibleTypeNames) { + if (null === $possibleTypeNames) { $this->_possibleTypeNames = $possibleTypeNames = array_reduce($this->getPossibleTypes(), function(&$map, Type $possibleType) { $map[$possibleType->name] = true; return $map; @@ -98,11 +98,13 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT /** * @param $value - * @return ObjectType|null + * @param ResolveInfo $info + * @return Type|null + * @throws \Exception */ - public function resolveType($value) + public function getObjectType($value, ResolveInfo $info) { $resolver = $this->_resolveType; - return $resolver ? call_user_func($resolver, $value) : Type::getTypeOf($value, $this); + return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this); } } diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index acf8607..0378e59 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -45,7 +45,7 @@ class ObjectType extends Type implements OutputType, CompositeType /** * @var array */ - private $_fields = []; + private $_fields; /** * @var array @@ -57,9 +57,46 @@ class ObjectType extends Type implements OutputType, CompositeType */ private $_isTypeOf; + /** + * Keeping reference of config for late bindings + * + * @var array + */ + private $_config; + + private $_initialized = false; + public function __construct(array $config) { - Config::validate($config, [ + $this->name = $config['name']; + $this->description = isset($config['description']) ? $config['description'] : null; + $this->_config = $config; + + if (isset($config['interfaces'])) { + InterfaceType::addImplementationToInterfaces($this); + } + } + + /** + * Late instance initialization + */ + private function initialize() + { + if ($this->_initialized) { + return ; + } + $config = $this->_config; + + if (isset($config['fields']) && is_callable($config['fields'])) { + $config['fields'] = call_user_func($config['fields']); + } + if (isset($config['interfaces']) && is_callable($config['interfaces'])) { + $config['interfaces'] = call_user_func($config['interfaces']); + } + + // Note: this validation is disabled by default, because it is resource-consuming + // TODO: add bin/validate script to check if schema is valid during development + Config::validate($this->_config, [ 'name' => Config::STRING | Config::REQUIRED, 'fields' => Config::arrayOf( FieldDefinition::getDefinition(), @@ -69,22 +106,13 @@ class ObjectType extends Type implements OutputType, CompositeType 'interfaces' => Config::arrayOf( Config::INTERFACE_TYPE ), - 'isTypeOf' => Config::CALLBACK, + 'isTypeOf' => Config::CALLBACK, // ($value, ResolveInfo $info) => boolean ]); - $this->name = $config['name']; - $this->description = isset($config['description']) ? $config['description'] : null; - - if (isset($config['fields'])) { - $this->_fields = FieldDefinition::createMap($config['fields']); - } - + $this->_fields = FieldDefinition::createMap($config['fields']); $this->_interfaces = isset($config['interfaces']) ? $config['interfaces'] : []; $this->_isTypeOf = isset($config['isTypeOf']) ? $config['isTypeOf'] : null; - - if (!empty($this->_interfaces)) { - InterfaceType::addImplementationToInterfaces($this, $this->_interfaces); - } + $this->_initialized = true; } /** @@ -92,6 +120,9 @@ class ObjectType extends Type implements OutputType, CompositeType */ public function getFields() { + if (false === $this->_initialized) { + $this->initialize(); + } return $this->_fields; } @@ -102,6 +133,9 @@ class ObjectType extends Type implements OutputType, CompositeType */ public function getField($name) { + if (false === $this->_initialized) { + $this->initialize(); + } Utils::invariant(isset($this->_fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name); return $this->_fields[$name]; } @@ -111,6 +145,9 @@ class ObjectType extends Type implements OutputType, CompositeType */ public function getInterfaces() { + if (false === $this->_initialized) { + $this->initialize(); + } return $this->_interfaces; } @@ -118,8 +155,8 @@ class ObjectType extends Type implements OutputType, CompositeType * @param $value * @return bool|null */ - public function isTypeOf($value) + public function isTypeOf($value, ResolveInfo $info) { - return isset($this->_isTypeOf) ? call_user_func($this->_isTypeOf, $value) : null; + return isset($this->_isTypeOf) ? call_user_func($this->_isTypeOf, $value, $info) : null; } } diff --git a/src/Type/Definition/ResolveInfo.php b/src/Type/Definition/ResolveInfo.php new file mode 100644 index 0000000..8313d7b --- /dev/null +++ b/src/Type/Definition/ResolveInfo.php @@ -0,0 +1,61 @@ + + */ + public $fragments; + + /** + * @var mixed + */ + public $rootValue; + + /** + * @var OperationDefinition + */ + public $operation; + + /** + * @var array + */ + public $variableValues; + + public function __construct(array $values) + { + Utils::assign($this, $values); + } +} diff --git a/src/Type/Definition/ScalarType.php b/src/Type/Definition/ScalarType.php index 04ad3da..cc840ca 100644 --- a/src/Type/Definition/ScalarType.php +++ b/src/Type/Definition/ScalarType.php @@ -14,7 +14,7 @@ use GraphQL\Utils; * * var OddType = new GraphQLScalarType({ * name: 'Odd', - * coerce(value) { + * serialize(value) { * return value % 2 === 1 ? value : null; * } * }); @@ -27,7 +27,9 @@ abstract class ScalarType extends Type implements OutputType, InputType Utils::invariant($this->name, 'Type must be named.'); } - abstract public function coerce($value); + abstract public function serialize($value); - abstract public function coerceLiteral($ast); + abstract public function parseValue($value); + + abstract public function parseLiteral(/* GraphQL\Language\AST\Value */$valueAST); } diff --git a/src/Type/Definition/ScalarTypeConfig.php b/src/Type/Definition/ScalarTypeConfig.php deleted file mode 100644 index eb5255d..0000000 --- a/src/Type/Definition/ScalarTypeConfig.php +++ /dev/null @@ -1,26 +0,0 @@ -parseValue($value); + } + + public function parseValue($value) { if ($value === true) { return 'true'; @@ -18,7 +23,7 @@ class StringType extends ScalarType return (string) $value; } - public function coerceLiteral($ast) + public function parseLiteral($ast) { if ($ast instanceof StringValue) { return $ast->value; diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 2bf188d..22e90d9 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -111,7 +111,7 @@ GraphQLNonNull; */ public static function isInputType($type) { - $nakedType = self::getUnmodifiedType($type); + $nakedType = self::getNamedType($type); return $nakedType instanceof InputType; } @@ -121,13 +121,14 @@ GraphQLNonNull; */ public static function isOutputType($type) { - $nakedType = self::getUnmodifiedType($type); + $nakedType = self::getNamedType($type); return $nakedType instanceof OutputType; } public static function isLeafType($type) { - $nakedType = self::getUnmodifiedType($type); + // TODO: add LeafType interface + $nakedType = self::getNamedType($type); return ( $nakedType instanceof ScalarType || $nakedType instanceof EnumType @@ -136,19 +137,12 @@ GraphQLNonNull; public static function isCompositeType($type) { - return ( - $type instanceof ObjectType || - $type instanceof InterfaceType || - $type instanceof UnionType - ); + return $type instanceof CompositeType; } public static function isAbstractType($type) { - return ( - $type instanceof InterfaceType || - $type instanceof UnionType - ); + return $type instanceof AbstractType; } /** @@ -164,7 +158,7 @@ GraphQLNonNull; * @param $type * @return UnmodifiedType */ - public static function getUnmodifiedType($type) + public static function getNamedType($type) { if (null === $type) { return null; @@ -195,21 +189,21 @@ GraphQLNonNull; * @return Type * @throws \Exception */ - public static function getTypeOf($value, AbstractType $abstractType) + public static function getTypeOf($value, ResolveInfo $info, AbstractType $abstractType) { $possibleTypes = $abstractType->getPossibleTypes(); for ($i = 0; $i < count($possibleTypes); $i++) { /** @var ObjectType $type */ $type = $possibleTypes[$i]; - $isTypeOf = $type->isTypeOf($value); + $isTypeOf = $type->isTypeOf($value, $info); if ($isTypeOf === null) { // TODO: move this to a JS impl specific type system validation step // so the error can be found before execution. throw new \Exception( 'Non-Object Type ' . $abstractType->name . ' does not implement ' . - 'resolveType and Object Type ' . $type->name . ' does not implement ' . + 'getObjectType and Object Type ' . $type->name . ' does not implement ' . 'isTypeOf. There is no way to determine if a value is of this type.' ); } diff --git a/src/Type/Definition/UnionType.php b/src/Type/Definition/UnionType.php index 529b991..c72de31 100644 --- a/src/Type/Definition/UnionType.php +++ b/src/Type/Definition/UnionType.php @@ -73,9 +73,9 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType * @param ObjectType $value * @return Type */ - public function resolveType($value) + public function getObjectType($value, ResolveInfo $info) { $resolver = $this->_resolveType; - return $resolver ? call_user_func($resolver, $value) : Type::getTypeOf($value, $this); + return $resolver ? call_user_func($resolver, $value) : Type::getTypeOf($value, $info, $this); } } diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index 002c5f6..dbd85fb 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -3,6 +3,7 @@ namespace GraphQL\Type; use GraphQL\Schema; +use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InputObjectType; @@ -10,6 +11,7 @@ use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; @@ -30,6 +32,165 @@ class Introspection { private static $_map = []; + /** + * @return string + */ + public static function getIntrospectionQuery($includeDescription = true) + { + $withDescription = <<<'EOD' + query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + types { + ...FullType + } + directives { + name + description + args { + ...InputValue + } + onOperation + onFragment + onField + } + } + } + + fragment FullType on __Type { + kind + name + description + fields { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + + fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue + } + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } +EOD; + $withoutDescription = <<<'EOD' + query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + types { + ...FullType + } + directives { + name + args { + ...InputValue + } + onOperation + onFragment + onField + } + } + } + + fragment FullType on __Type { + kind + name + fields { + name + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues { + name + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + + fragment InputValue on __InputValue { + name + type { ...TypeRef } + defaultValue + } + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } +EOD; + return $includeDescription ? $withDescription : $withoutDescription; + } + public static function _schema() { if (!isset(self::$_map['__Schema'])) { @@ -83,12 +244,15 @@ class Introspection self::$_map['__Directive'] = new ObjectType([ 'name' => '__Directive', 'fields' => [ - 'name' => ['type' => Type::string()], + 'name' => ['type' => Type::nonNull(Type::string())], 'description' => ['type' => Type::string()], - 'type' => ['type' => [__CLASS__, '_type']], - 'onOperation' => ['type' => Type::boolean()], - 'onFragment' => ['type' => Type::boolean()], - 'onField' => ['type' => Type::boolean()] + 'args' => [ + 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), + 'resolve' => function(Directive $directive) {return $directive->args ?: [];} + ], + 'onOperation' => ['type' => Type::nonNull(Type::boolean())], + 'onFragment' => ['type' => Type::nonNull(Type::boolean())], + 'onField' => ['type' => Type::nonNull(Type::boolean())] ] ]); } @@ -349,14 +513,9 @@ class Introspection 'resolve' => function ( $source, $args, - $root, - $fieldAST, - $fieldType, - $parentType, - $schema + ResolveInfo $info ) { - // TODO: move 3+ args to separate object - return $schema; + return $info->schema; } ]); } @@ -373,8 +532,8 @@ class Introspection 'args' => [ ['name' => 'name', 'type' => Type::nonNull(Type::string())] ], - 'resolve' => function ($source, $args, $root, $fieldAST, $fieldType, $parentType, $schema) { - return $schema->getType($args['name']); + 'resolve' => function ($source, $args, ResolveInfo $info) { + return $info->schema->getType($args['name']); } ]); } @@ -392,12 +551,9 @@ class Introspection 'resolve' => function ( $source, $args, - $root, - $fieldAST, - $fieldType, - $parentType + ResolveInfo $info ) { - return $parentType->name; + return $info->parentType->name; } ]); } diff --git a/src/Type/SchemaValidator.php b/src/Type/SchemaValidator.php index 04131d0..adc8ffb 100644 --- a/src/Type/SchemaValidator.php +++ b/src/Type/SchemaValidator.php @@ -169,11 +169,6 @@ class SchemaValidator $errors = array_merge($errors, $newErrors); } } - $isValid = empty($errors); - $result = [ - 'isValid' => $isValid, - 'errors' => $isValid ? null : array_map(['GraphQL\Error', 'formatError'], $errors) - ]; - return (object) $result; + return $errors; } } \ No newline at end of file diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index fc1a09f..46ed40c 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -4,9 +4,12 @@ namespace GraphQL\Utils; use GraphQL\Language\AST\Field; use GraphQL\Language\AST\ListType; use GraphQL\Language\AST\Name; +use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NonNullType; use GraphQL\Schema; +use GraphQL\Type\Definition\Directive; +use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputType; @@ -38,8 +41,8 @@ class TypeInfo return $innerType ? new NonNull($innerType) : null; } - Utils::invariant($inputTypeAst instanceof Name, 'Must be a type name'); - return $schema->getType($inputTypeAst->value); + Utils::invariant($inputTypeAst->kind === Node::NAMED_TYPE, 'Must be a named type'); + return $schema->getType($inputTypeAst->name->value); } /** @@ -103,6 +106,15 @@ class TypeInfo */ private $_fieldDefStack; + /** + * @var Directive + */ + private $_directive; + + /** + * @var FieldArgument + */ + private $_argument; public function __construct(Schema $schema) { @@ -157,6 +169,21 @@ class TypeInfo return null; } + /** + * @return Directive|null + */ + function getDirective() + { + return $this->_directive; + } + + /** + * @return FieldArgument|null + */ + function getArgument() + { + return $this->_argument; + } function enter(Node $node) { @@ -164,16 +191,19 @@ class TypeInfo switch ($node->kind) { case Node::SELECTION_SET: - // var $compositeType: ?GraphQLCompositeType; - $rawType = Type::getUnmodifiedType($this->getType()); + $namedType = Type::getNamedType($this->getType()); $compositeType = null; - if (Type::isCompositeType($rawType)) { + if (Type::isCompositeType($namedType)) { // isCompositeType is a type refining predicate, so this is safe. - $compositeType = $rawType; + $compositeType = $namedType; } array_push($this->_parentTypeStack, $compositeType); break; + case Node::DIRECTIVE: + $this->_directive = $schema->getDirective($node->name->value); + break; + case Node::FIELD: $parentType = $this->getParentType(); $fieldDef = null; @@ -196,7 +226,7 @@ class TypeInfo case Node::INLINE_FRAGMENT: case Node::FRAGMENT_DEFINITION: - $type = $schema->getType($node->typeCondition->value); + $type = self::typeFromAST($schema, $node->typeCondition); array_push($this->_typeStack, $type); break; @@ -205,32 +235,28 @@ class TypeInfo break; case Node::ARGUMENT: - $field = $this->getFieldDef(); - $argType = null; - if ($field) { - $argDef = Utils::find($field->args, function($arg) use ($node) {return $arg->name === $node->name->value;}); + $fieldOrDirective = $this->getDirective() ?: $this->getFieldDef(); + $argDef = $argType = null; + if ($fieldOrDirective) { + $argDef = Utils::find($fieldOrDirective->args, function($arg) use ($node) {return $arg->name === $node->name->value;}); if ($argDef) { $argType = $argDef->getType(); } } + $this->_argument = $argDef; array_push($this->_inputTypeStack, $argType); break; - case Node::DIRECTIVE: - $directive = $schema->getDirective($node->name->value); - array_push($this->_inputTypeStack, $directive ? $directive->type : null); - break; - case Node::LST: - $arrayType = Type::getNullableType($this->getInputType()); + $listType = Type::getNullableType($this->getInputType()); array_push( $this->_inputTypeStack, - $arrayType instanceof ListOfType ? $arrayType->getWrappedType() : null + $listType instanceof ListOfType ? $listType->getWrappedType() : null ); break; case Node::OBJECT_FIELD: - $objectType = Type::getUnmodifiedType($this->getInputType()); + $objectType = Type::getNamedType($this->getInputType()); $fieldType = null; if ($objectType instanceof InputObjectType) { $tmp = $objectType->getFields(); @@ -248,10 +274,16 @@ class TypeInfo case Node::SELECTION_SET: array_pop($this->_parentTypeStack); break; + case Node::FIELD: array_pop($this->_fieldDefStack); array_pop($this->_typeStack); break; + + case Node::DIRECTIVE: + $this->_directive = null; + break; + case Node::OPERATION_DEFINITION: case Node::INLINE_FRAGMENT: case Node::FRAGMENT_DEFINITION: @@ -261,9 +293,9 @@ class TypeInfo array_pop($this->_inputTypeStack); break; case Node::ARGUMENT: + $this->_argument = null; array_pop($this->_inputTypeStack); break; - case Node::DIRECTIVE: case Node::LST: case Node::OBJECT_FIELD: array_pop($this->_inputTypeStack); diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 2aa9d43..05157a1 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -2,7 +2,7 @@ namespace GraphQL\Validator; use GraphQL\Error; -use GraphQL\Language\AST\ArrayValue; +use GraphQL\Language\AST\ListValue; use GraphQL\Language\AST\Document; use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\Node; @@ -33,6 +33,7 @@ use GraphQL\Validator\Rules\NoUnusedFragments; use GraphQL\Validator\Rules\NoUnusedVariables; use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged; use GraphQL\Validator\Rules\PossibleFragmentSpreads; +use GraphQL\Validator\Rules\ProvidedNonNullArguments; use GraphQL\Validator\Rules\ScalarLeafs; use GraphQL\Validator\Rules\VariablesAreInputTypes; use GraphQL\Validator\Rules\VariablesInAllowedPosition; @@ -45,23 +46,28 @@ class DocumentValidator { if (null === self::$allRules) { self::$allRules = [ - new ArgumentsOfCorrectType(), - new DefaultValuesOfCorrectType(), - new FieldsOnCorrectType(), - new FragmentsOnCompositeTypes(), - new KnownArgumentNames(), - new KnownDirectives(), - new KnownFragmentNames(), - new KnownTypeNames(), - new NoFragmentCycles(), - new NoUndefinedVariables(), - new NoUnusedFragments(), - new NoUnusedVariables(), - new OverlappingFieldsCanBeMerged(), - new PossibleFragmentSpreads(), - new ScalarLeafs(), - new VariablesAreInputTypes(), - new VariablesInAllowedPosition() + // new UniqueOperationNames, + // new LoneAnonymousOperation, + new KnownTypeNames, + new FragmentsOnCompositeTypes, + new VariablesAreInputTypes, + new ScalarLeafs, + new FieldsOnCorrectType, + // new UniqueFragmentNames, + new KnownFragmentNames, + new NoUnusedFragments, + new PossibleFragmentSpreads, + new NoFragmentCycles, + new NoUndefinedVariables, + new NoUnusedVariables, + new KnownDirectives, + new KnownArgumentNames, + // new UniqueArgumentNames, + new ArgumentsOfCorrectType, + new ProvidedNonNullArguments, + new DefaultValuesOfCorrectType, + new VariablesInAllowedPosition, + new OverlappingFieldsCanBeMerged, ]; } return self::$allRules; @@ -70,13 +76,7 @@ class DocumentValidator public static function validate(Schema $schema, Document $ast, array $rules = null) { $errors = self::visitUsingRules($schema, $ast, $rules ?: self::allRules()); - $isValid = empty($errors); - - $result = [ - 'isValid' => $isValid, - 'errors' => $isValid ? null : array_map(['GraphQL\Error', 'formatError'], $errors) - ]; - return $result; + return $errors; } static function isError($value) @@ -121,7 +121,7 @@ class DocumentValidator // Lists accept a non-list value as a list of one. if ($type instanceof ListOfType) { $itemType = $type->getWrappedType(); - if ($valueAST instanceof ArrayValue) { + if ($valueAST instanceof ListValue) { foreach($valueAST->values as $itemAST) { if (!self::isValidLiteralValue($itemAST, $itemType)) { return false; @@ -133,10 +133,10 @@ class DocumentValidator } } - // Scalar/Enum input checks to ensure the type can coerce the value to + // Scalar/Enum input checks to ensure the type can serialize the value to // a non-null value. if ($type instanceof ScalarType || $type instanceof EnumType) { - return $type->coerceLiteral($valueAST) !== null; + return $type->parseLiteral($valueAST) !== null; } // Input objects check each defined field, ensuring it is of the correct @@ -294,7 +294,7 @@ class DocumentValidator if ($result->doBreak) { $instances[$i] = null; } - } if (self::isError($result)) { + } else if (self::isError($result)) { self::append($errors, $result); } else if ($result !== null) { throw new \Exception("Config cannot edit document."); diff --git a/src/Validator/Rules/ArgumentsOfCorrectType.php b/src/Validator/Rules/ArgumentsOfCorrectType.php index d634828..8191c03 100644 --- a/src/Validator/Rules/ArgumentsOfCorrectType.php +++ b/src/Validator/Rules/ArgumentsOfCorrectType.php @@ -16,52 +16,22 @@ use GraphQL\Validator\ValidationContext; class ArgumentsOfCorrectType { + static function badValueMessage($argName, $type, $value) + { + return "Argument \"$argName\" expected type \"$type\" but got: $value."; + } + public function __invoke(ValidationContext $context) { return [ - Node::FIELD => function(Field $fieldAST) use ($context) { - $fieldDef = $context->getFieldDef(); - if (!$fieldDef) { - return Visitor::skipNode(); + Node::ARGUMENT => function(Argument $argAST) use ($context) { + $argDef = $context->getArgument(); + if ($argDef && !DocumentValidator::isValidLiteralValue($argAST->value, $argDef->getType())) { + return new Error( + self::badValueMessage($argAST->name->value, $argDef->getType(), Printer::doPrint($argAST->value)), + [$argAST->value] + ); } - $errors = []; - $argASTs = $fieldAST->arguments ?: []; - $argASTMap = Utils::keyMap($argASTs, function (Argument $arg) { - return $arg->name->value; - }); - - foreach ($fieldDef->args as $argDef) { - $argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null; - if (!$argAST && $argDef->getType() instanceof NonNull) { - $errors[] = new Error( - Messages::missingArgMessage( - $fieldAST->name->value, - $argDef->name, - $argDef->getType() - ), - [$fieldAST] - ); - } - } - - $argDefMap = Utils::keyMap($fieldDef->args, function ($def) { - return $def->name; - }); - foreach ($argASTs as $argAST) { - $argDef = $argDefMap[$argAST->name->value]; - if ($argDef && !DocumentValidator::isValidLiteralValue($argAST->value, $argDef->getType())) { - $errors[] = new Error( - Messages::badValueMessage( - $argAST->name->value, - $argDef->getType(), - Printer::doPrint($argAST->value) - ), - [$argAST->value] - ); - } - } - - return !empty($errors) ? $errors : null; } ]; } diff --git a/src/Validator/Rules/FragmentsOnCompositeTypes.php b/src/Validator/Rules/FragmentsOnCompositeTypes.php index b6fe572..f3cee89 100644 --- a/src/Validator/Rules/FragmentsOnCompositeTypes.php +++ b/src/Validator/Rules/FragmentsOnCompositeTypes.php @@ -6,35 +6,43 @@ use GraphQL\Error; use GraphQL\Language\AST\FragmentDefinition; use GraphQL\Language\AST\InlineFragment; use GraphQL\Language\AST\Node; +use GraphQL\Language\Printer; use GraphQL\Type\Definition\CompositeType; +use GraphQL\Type\Definition\Type; use GraphQL\Validator\Messages; use GraphQL\Validator\ValidationContext; class FragmentsOnCompositeTypes { + static function inlineFragmentOnNonCompositeErrorMessage($type) + { + return "Fragment cannot condition on non composite type \"$type\"."; + } + + static function fragmentOnNonCompositeErrorMessage($fragName, $type) + { + return "Fragment \"$fragName\" cannot condition on non composite type \"$type\"."; + } + public function __invoke(ValidationContext $context) { return [ Node::INLINE_FRAGMENT => function(InlineFragment $node) use ($context) { - $typeName = $node->typeCondition->value; - $type = $context->getSchema()->getType($typeName); - $isCompositeType = $type instanceof CompositeType; + $type = $context->getType(); - if (!$isCompositeType) { + if ($type && !Type::isCompositeType($type)) { return new Error( - "Fragment cannot condition on non composite type \"$typeName\".", + self::inlineFragmentOnNonCompositeErrorMessage($type), [$node->typeCondition] ); } }, Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) use ($context) { - $typeName = $node->typeCondition->value; - $type = $context->getSchema()->getType($typeName); - $isCompositeType = $type instanceof CompositeType; + $type = $context->getType(); - if (!$isCompositeType) { + if ($type && !Type::isCompositeType($type)) { return new Error( - Messages::fragmentOnNonCompositeErrorMessage($node->name->value, $typeName), + self::fragmentOnNonCompositeErrorMessage($node->name->value, Printer::doPrint($node->typeCondition)), [$node->typeCondition] ); } diff --git a/src/Validator/Rules/KnownArgumentNames.php b/src/Validator/Rules/KnownArgumentNames.php index 84e7e5c..c610573 100644 --- a/src/Validator/Rules/KnownArgumentNames.php +++ b/src/Validator/Rules/KnownArgumentNames.php @@ -11,27 +11,57 @@ use GraphQL\Validator\ValidationContext; class KnownArgumentNames { + public static function unknownArgMessage($argName, $fieldName, $type) + { + return "Unknown argument \"$argName\" on field \"$fieldName\" of type \"$type\"."; + } + + public static function unknownDirectiveArgMessage($argName, $directiveName) + { + return "Unknown argument \"$argName\" on directive \"@$directiveName\"."; + } + public function __invoke(ValidationContext $context) { return [ - Node::ARGUMENT => function(Argument $node) use ($context) { - $fieldDef = $context->getFieldDef(); - if ($fieldDef) { - $argDef = null; - foreach ($fieldDef->args as $arg) { - if ($arg->name === $node->name->value) { - $argDef = $arg; - break; + Node::ARGUMENT => function(Argument $node, $key, $parent, $path, $ancestors) use ($context) { + $argumentOf = $ancestors[count($ancestors) - 1]; + if ($argumentOf->kind === Node::FIELD) { + $fieldDef = $context->getFieldDef(); + + if ($fieldDef) { + $fieldArgDef = null; + foreach ($fieldDef->args as $arg) { + if ($arg->name === $node->name->value) { + $fieldArgDef = $arg; + break; + } + } + if (!$fieldArgDef) { + $parentType = $context->getParentType(); + Utils::invariant($parentType); + return new Error( + self::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name), + [$node] + ); } } - - if (!$argDef) { - $parentType = $context->getParentType(); - Utils::invariant($parentType); - return new Error( - Messages::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name), - [$node] - ); + } else if ($argumentOf->kind === Node::DIRECTIVE) { + $directive = $context->getDirective(); + if ($directive) { + $directiveArgDef = null; + foreach ($directive->args as $arg) { + if ($arg->name === $node->name->value) { + $directiveArgDef = $arg; + break; + } + } + if (!$directiveArgDef) { + return new Error( + self::unknownDirectiveArgMessage($node->name->value, $directive->name), + [$node] + ); + } } } } diff --git a/src/Validator/Rules/KnownDirectives.php b/src/Validator/Rules/KnownDirectives.php index eacc820..da8d3af 100644 --- a/src/Validator/Rules/KnownDirectives.php +++ b/src/Validator/Rules/KnownDirectives.php @@ -15,6 +15,16 @@ use GraphQL\Validator\ValidationContext; class KnownDirectives { + static function unknownDirectiveMessage($directiveName) + { + return "Unknown directive \"$directiveName\"."; + } + + static function misplacedDirectiveMessage($directiveName, $placement) + { + return "Directive \"$directiveName\" may not be used on \"$placement\"."; + } + public function __invoke(ValidationContext $context) { return [ @@ -29,7 +39,7 @@ class KnownDirectives if (!$directiveDef) { return new Error( - Messages::unknownDirectiveMessage($node->name->value), + self::unknownDirectiveMessage($node->name->value), [$node] ); } @@ -37,13 +47,13 @@ class KnownDirectives if ($appliedTo instanceof OperationDefinition && !$directiveDef->onOperation) { return new Error( - Messages::misplacedDirectiveMessage($node->name->value, 'operation'), + self::misplacedDirectiveMessage($node->name->value, 'operation'), [$node] ); } if ($appliedTo instanceof Field && !$directiveDef->onField) { return new Error( - Messages::misplacedDirectiveMessage($node->name->value, 'field'), + self::misplacedDirectiveMessage($node->name->value, 'field'), [$node] ); } @@ -56,7 +66,7 @@ class KnownDirectives if ($fragmentKind && !$directiveDef->onFragment) { return new Error( - Messages::misplacedDirectiveMessage($node->name->value, 'fragment'), + self::misplacedDirectiveMessage($node->name->value, 'fragment'), [$node] ); } diff --git a/src/Validator/Rules/KnownFragmentNames.php b/src/Validator/Rules/KnownFragmentNames.php index 5cc0495..3d0dbc8 100644 --- a/src/Validator/Rules/KnownFragmentNames.php +++ b/src/Validator/Rules/KnownFragmentNames.php @@ -9,6 +9,11 @@ use GraphQL\Validator\ValidationContext; class KnownFragmentNames { + static function unknownFragmentMessage($fragName) + { + return "Unknown fragment \"$fragName\"."; + } + public function __invoke(ValidationContext $context) { return [ @@ -17,7 +22,7 @@ class KnownFragmentNames $fragment = $context->getFragment($fragmentName); if (!$fragment) { return new Error( - "Undefined fragment $fragmentName.", + self::unknownFragmentMessage($fragmentName), [$node->name] ); } diff --git a/src/Validator/Rules/KnownTypeNames.php b/src/Validator/Rules/KnownTypeNames.php index 91b9c46..93d5f4c 100644 --- a/src/Validator/Rules/KnownTypeNames.php +++ b/src/Validator/Rules/KnownTypeNames.php @@ -4,22 +4,27 @@ namespace GraphQL\Validator\Rules; use GraphQL\Error; use GraphQL\Language\AST\Name; +use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\Node; use GraphQL\Validator\Messages; use GraphQL\Validator\ValidationContext; class KnownTypeNames { + static function unknownTypeMessage($type) + { + return "Unknown type \"$type\"."; + } + public function __invoke(ValidationContext $context) { return [ - Node::NAME => function(Name $node, $key) use ($context) { - + Node::NAMED_TYPE => function(NamedType $node, $key) use ($context) { if ($key === 'type' || $key === 'typeCondition') { - $typeName = $node->value; + $typeName = $node->name->value; $type = $context->getSchema()->getType($typeName); if (!$type) { - return new Error(Messages::unknownTypeMessage($typeName), [$node]); + return new Error(self::unknownTypeMessage($typeName), [$node]); } } } diff --git a/src/Validator/Rules/NoFragmentCycles.php b/src/Validator/Rules/NoFragmentCycles.php index bf9bb15..8f41fb8 100644 --- a/src/Validator/Rules/NoFragmentCycles.php +++ b/src/Validator/Rules/NoFragmentCycles.php @@ -14,11 +14,16 @@ use GraphQL\Language\AST\FragmentDefinition; use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\Node; use GraphQL\Language\Visitor; -use GraphQL\Validator\Messages; use GraphQL\Validator\ValidationContext; class NoFragmentCycles { + static function cycleErrorMessage($fragName, array $spreadNames = []) + { + $via = !empty($spreadNames) ? ' via ' . implode(', ', $spreadNames) : ''; + return "Cannot spread fragment \"$fragName\" within itself$via."; + } + public function __invoke(ValidationContext $context) { // Gather all the fragment spreads ASTs for each fragment definition. @@ -67,7 +72,7 @@ class NoFragmentCycles $knownToLeadToCycle[$spread] = true; } $errors[] = new Error( - Messages::cycleErrorMessage($initialName, array_map(function ($s) { + self::cycleErrorMessage($initialName, array_map(function ($s) { return $s->name->value; }, $spreadPath)), $cyclePath diff --git a/src/Validator/Rules/NoUndefinedVariables.php b/src/Validator/Rules/NoUndefinedVariables.php index 56a3c70..dca3ad9 100644 --- a/src/Validator/Rules/NoUndefinedVariables.php +++ b/src/Validator/Rules/NoUndefinedVariables.php @@ -23,6 +23,16 @@ use GraphQL\Validator\ValidationContext; */ class NoUndefinedVariables { + static function undefinedVarMessage($varName) + { + return "Variable \"$$varName\" is not defined."; + } + + static function undefinedVarByOpMessage($varName, $opName) + { + return "Variable \"$$varName\" is not defined by operation \"$opName\"."; + } + public function __invoke(ValidationContext $context) { $operation = null; @@ -53,12 +63,12 @@ class NoUndefinedVariables } if ($withinFragment && $operation && $operation->name) { return new Error( - Messages::undefinedVarByOpMessage($varName, $operation->name->value), + self::undefinedVarByOpMessage($varName, $operation->name->value), [$variable, $operation] ); } return new Error( - Messages::undefinedVarMessage($varName), + self::undefinedVarMessage($varName), [$variable] ); } diff --git a/src/Validator/Rules/NoUnusedFragments.php b/src/Validator/Rules/NoUnusedFragments.php index 99d10ea..bea8422 100644 --- a/src/Validator/Rules/NoUnusedFragments.php +++ b/src/Validator/Rules/NoUnusedFragments.php @@ -11,6 +11,11 @@ use GraphQL\Validator\ValidationContext; class NoUnusedFragments { + static function unusedFragMessage($fragName) + { + return "Fragment \"$fragName\" is never used."; + } + public function __invoke(ValidationContext $context) { $fragmentDefs = []; @@ -43,7 +48,7 @@ class NoUnusedFragments foreach ($fragmentDefs as $def) { if (empty($fragmentNameUsed[$def->name->value])) { $errors[] = new Error( - Messages::unusedFragMessage($def->name->value), + self::unusedFragMessage($def->name->value), [$def] ); } @@ -59,11 +64,14 @@ class NoUnusedFragments foreach ($spreads as $fragName => $fragment) { if (empty($fragmentNameUsed[$fragName])) { $fragmentNameUsed[$fragName] = true; - $this->reduceSpreadFragments( - $fragAdjacencies->{$fragName}, - $fragmentNameUsed, - $fragAdjacencies - ); + + if (isset($fragAdjacencies->{$fragName})) { + $this->reduceSpreadFragments( + $fragAdjacencies->{$fragName}, + $fragmentNameUsed, + $fragAdjacencies + ); + } } } } diff --git a/src/Validator/Rules/NoUnusedVariables.php b/src/Validator/Rules/NoUnusedVariables.php index 7299153..9a63a80 100644 --- a/src/Validator/Rules/NoUnusedVariables.php +++ b/src/Validator/Rules/NoUnusedVariables.php @@ -10,6 +10,11 @@ use GraphQL\Validator\ValidationContext; class NoUnusedVariables { + static function unusedVariableMessage($varName) + { + return "Variable \"$$varName\" is never used."; + } + public function __invoke(ValidationContext $context) { $visitedFragmentNames = new \stdClass(); @@ -30,7 +35,7 @@ class NoUnusedVariables foreach ($variableDefs as $def) { if (empty($variableNameUsed->{$def->variable->name->value})) { $errors[] = new Error( - Messages::unusedVariableMessage($def->variable->name->value), + self::unusedVariableMessage($def->variable->name->value), [$def] ); } diff --git a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php index c185a3c..fb27759 100644 --- a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php +++ b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php @@ -3,19 +3,40 @@ namespace GraphQL\Validator\Rules; use GraphQL\Error; +use GraphQL\Language\AST\Directive; use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\InlineFragment; +use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\SelectionSet; use GraphQL\Language\Printer; use GraphQL\Type\Definition\Type; +use GraphQL\Utils; use GraphQL\Utils\PairSet; use GraphQL\Utils\TypeInfo; -use GraphQL\Validator\Messages; use GraphQL\Validator\ValidationContext; class OverlappingFieldsCanBeMerged { + static function fieldsConflictMessage($responseName, $reason) + { + $reasonMessage = self::reasonMessage($reason); + return "Fields \"$responseName\" conflict because $reasonMessage."; + } + + static function reasonMessage($reason) + { + if (is_array($reason)) { + $tmp = array_map(function ($tmp) { + list($responseName, $subReason) = $tmp; + $reasonMessage = self::reasonMessage($subReason); + return "subfields \"$responseName\" conflict because $reasonMessage"; + }, $reason); + return implode(' and ', $tmp); + } + return $reason; + } + public function __invoke(ValidationContext $context) { $comparedSet = new PairSet(); @@ -27,7 +48,7 @@ class OverlappingFieldsCanBeMerged 'leave' => function(SelectionSet $selectionSet) use ($context, $comparedSet) { $fieldMap = $this->collectFieldASTsAndDefs( $context, - $context->getType(), + $context->getParentType(), $selectionSet ); @@ -37,11 +58,11 @@ class OverlappingFieldsCanBeMerged return array_map(function ($conflict) { $responseName = $conflict[0][0]; $reason = $conflict[0][1]; - $blameNodes = $conflict[1]; + $fields = $conflict[1]; return new Error( - Messages::fieldsConflictMessage($responseName, $reason), - $blameNodes + self::fieldsConflictMessage($responseName, $reason), + $fields ); }, $conflicts); @@ -101,7 +122,7 @@ class OverlappingFieldsCanBeMerged $type1 = isset($def1) ? $def1->getType() : null; $type2 = isset($def2) ? $def2->getType() : null; - if (!$this->sameType($type1, $type2)) { + if ($type1 && $type2 && !$this->sameType($type1, $type2)) { return [ [$responseName, "they return differing types $type1 and $type2"], [$ast1, $ast2] @@ -111,7 +132,7 @@ class OverlappingFieldsCanBeMerged $args1 = isset($ast1->arguments) ? $ast1->arguments : []; $args2 = isset($ast2->arguments) ? $ast2->arguments : []; - if (!$this->sameNameValuePairs($args1, $args2)) { + if (!$this->sameArguments($args1, $args2)) { return [ [$responseName, 'they have differing arguments'], [$ast1, $ast2] @@ -121,7 +142,7 @@ class OverlappingFieldsCanBeMerged $directives1 = isset($ast1->directives) ? $ast1->directives : []; $directives2 = isset($ast2->directives) ? $ast2->directives : []; - if (!$this->sameNameValuePairs($directives1, $directives2)) { + if (!$this->sameDirectives($directives1, $directives2)) { return [ [$responseName, 'they have differing directives'], [$ast1, $ast2] @@ -136,13 +157,13 @@ class OverlappingFieldsCanBeMerged $subfieldMap = $this->collectFieldASTsAndDefs( $context, - $type1, + Type::getNamedType($type1), $selectionSet1, $visitedFragmentNames ); $subfieldMap = $this->collectFieldASTsAndDefs( $context, - $type2, + Type::getNamedType($type2), $selectionSet2, $visitedFragmentNames, $subfieldMap @@ -152,7 +173,7 @@ class OverlappingFieldsCanBeMerged if (!empty($conflicts)) { return [ [$responseName, array_map(function ($conflict) { return $conflict[0]; }, $conflicts)], - array_reduce($conflicts, function ($list, $conflict) { return array_merge($list, $conflict[1]); }, [$ast1, $ast2]) + array_reduce($conflicts, function ($allFields, $conflict) { return array_merge($allFields, $conflict[1]); }, [$ast1, $ast2]) ]; } } @@ -183,8 +204,7 @@ class OverlappingFieldsCanBeMerged switch ($selection->kind) { case Node::FIELD: - $fieldAST = $selection; - $fieldName = $fieldAST->name->value; + $fieldName = $selection->name->value; $fieldDef = null; if ($parentType && method_exists($parentType, 'getFields')) { $tmp = $parentType->getFields(); @@ -192,28 +212,26 @@ class OverlappingFieldsCanBeMerged $fieldDef = $tmp[$fieldName]; } } - $responseName = $fieldAST->alias ? $fieldAST->alias->value : $fieldName; + $responseName = $selection->alias ? $selection->alias->value : $fieldName; if (!isset($_astAndDefs[$responseName])) { $_astAndDefs[$responseName] = new \ArrayObject(); } - $_astAndDefs[$responseName][] = [$fieldAST, $fieldDef]; + $_astAndDefs[$responseName][] = [$selection, $fieldDef]; break; case Node::INLINE_FRAGMENT: /** @var InlineFragment $inlineFragment */ - $inlineFragment = $selection; $_astAndDefs = $this->collectFieldASTsAndDefs( $context, - TypeInfo::typeFromAST($context->getSchema(), $inlineFragment->typeCondition), - $inlineFragment->selectionSet, + TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition), + $selection->selectionSet, $_visitedFragmentNames, $_astAndDefs ); break; case Node::FRAGMENT_SPREAD: - /** @var FragmentSpread $fragmentSpread */ - $fragmentSpread = $selection; - $fragName = $fragmentSpread->name->value; + /** @var FragmentSpread $selection */ + $fragName = $selection->name->value; if (!empty($_visitedFragmentNames[$fragName])) { continue; } @@ -235,29 +253,53 @@ class OverlappingFieldsCanBeMerged return $_astAndDefs; } + private function sameDirectives(array $directives1, array $directives2) + { + if (count($directives1) !== count($directives2)) { + return false; + } + + foreach ($directives1 as $directive1) { + $directive2 = null; + foreach ($directives2 as $tmp) { + if ($tmp->name->value === $directive1->name->value) { + $directive2 = $tmp; + break; + } + } + if (!$directive2) { + return false; + } + if (!$this->sameArguments($directive1->arguments, $directive2->arguments)) { + return false; + } + } + return true; + } + /** * @param Array $pairs1 * @param Array $pairs2 * @return bool|string */ - private function sameNameValuePairs(array $pairs1, array $pairs2) + private function sameArguments(array $arguments1, array $arguments2) { - if (count($pairs1) !== count($pairs2)) { + if (count($arguments1) !== count($arguments2)) { return false; } - foreach ($pairs1 as $pair1) { - $matchedPair2 = null; - foreach ($pairs2 as $pair2) { - if ($pair2->name->value === $pair1->name->value) { - $matchedPair2 = $pair2; + foreach ($arguments1 as $arg1) { + $arg2 = null; + foreach ($arguments2 as $arg) { + if ($arg->name->value === $arg1->name->value) { + $arg2 = $arg; break; } } - if (!$matchedPair2) { + if (!$arg2) { return false; } - if (!$this->sameValue($pair1->value, $matchedPair2->value)) { + if (!$this->sameValue($arg1->value, $arg2->value)) { return false; } } @@ -271,6 +313,6 @@ class OverlappingFieldsCanBeMerged function sameType($type1, $type2) { - return (!$type1 && !$type2) || (string) $type1 === (string) $type2; + return (string) $type1 === (string) $type2; } } diff --git a/src/Validator/Rules/PossibleFragmentSpreads.php b/src/Validator/Rules/PossibleFragmentSpreads.php index cb6fa9f..e4fcce0 100644 --- a/src/Validator/Rules/PossibleFragmentSpreads.php +++ b/src/Validator/Rules/PossibleFragmentSpreads.php @@ -11,32 +11,41 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Utils; -use GraphQL\Validator\Messages; use GraphQL\Validator\ValidationContext; class PossibleFragmentSpreads { + static function typeIncompatibleSpreadMessage($fragName, $parentType, $fragType) + { + return "Fragment \"$fragName\" cannot be spread here as objects of type \"$parentType\" can never be of type \"$fragType\"."; + } + + static function typeIncompatibleAnonSpreadMessage($parentType, $fragType) + { + return "Fragment cannot be spread here as objects of type \"$parentType\" can never be of type \"$fragType\"."; + } + public function __invoke(ValidationContext $context) { return [ Node::INLINE_FRAGMENT => function(InlineFragment $node) use ($context) { - $fragType = Type::getUnmodifiedType($context->getType()); + $fragType = Type::getNamedType($context->getType()); $parentType = $context->getParentType(); if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) { return new Error( - Messages::typeIncompatibleAnonSpreadMessage($parentType, $fragType), + self::typeIncompatibleAnonSpreadMessage($parentType, $fragType), [$node] ); } }, Node::FRAGMENT_SPREAD => function(FragmentSpread $node) use ($context) { $fragName = $node->name->value; - $fragType = Type::getUnmodifiedType($this->getFragmentType($context, $fragName)); + $fragType = Type::getNamedType($this->getFragmentType($context, $fragName)); $parentType = $context->getParentType(); if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) { return new Error( - Messages::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), + self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), [$node] ); } @@ -47,7 +56,7 @@ class PossibleFragmentSpreads private function getFragmentType(ValidationContext $context, $name) { $frag = $context->getFragment($name); - return $frag ? $context->getSchema()->getType($frag->typeCondition->value) : null; + return $frag ? Utils\TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition) : null; } private function doTypesOverlap($t1, $t2) diff --git a/src/Validator/Rules/ProvidedNonNullArguments.php b/src/Validator/Rules/ProvidedNonNullArguments.php new file mode 100644 index 0000000..f8595a0 --- /dev/null +++ b/src/Validator/Rules/ProvidedNonNullArguments.php @@ -0,0 +1,87 @@ + [ + 'leave' => function(Field $fieldAST) use ($context) { + $fieldDef = $context->getFieldDef(); + + if (!$fieldDef) { + return Visitor::skipNode(); + } + $errors = []; + $argASTs = $fieldAST->arguments ?: []; + + $argASTMap = []; + foreach ($argASTs as $argAST) { + $argASTMap[$argAST->name->value] = $argASTs; + } + foreach ($fieldDef->args as $argDef) { + $argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null; + if (!$argAST && $argDef->getType() instanceof NonNull) { + $errors[] = new Error( + self::missingFieldArgMessage($fieldAST->name->value, $argDef->name, $argDef->getType()), + [$fieldAST] + ); + } + } + + if (!empty($errors)) { + return $errors; + } + } + ], + Node::DIRECTIVE => [ + 'leave' => function(Directive $directiveAST) use ($context) { + $directiveDef = $context->getDirective(); + if (!$directiveDef) { + return Visitor::skipNode(); + } + $errors = []; + $argASTs = $directiveAST->arguments ?: []; + $argASTMap = []; + foreach ($argASTs as $argAST) { + $argASTMap[$argAST->name->value] = $argASTs; + } + + foreach ($directiveDef->args as $argDef) { + $argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null; + if (!$argAST && $argDef->getType() instanceof NonNull) { + $errors[] = new Error( + self::missingDirectiveArgMessage($directiveAST->name->value, $argDef->name, $argDef->getType()), + [$directiveAST] + ); + } + } + if (!empty($errors)) { + return $errors; + } + } + ] + ]; + } +} diff --git a/src/Validator/Rules/ScalarLeafs.php b/src/Validator/Rules/ScalarLeafs.php index 898aed3..58238bd 100644 --- a/src/Validator/Rules/ScalarLeafs.php +++ b/src/Validator/Rules/ScalarLeafs.php @@ -11,6 +11,16 @@ use GraphQL\Validator\ValidationContext; class ScalarLeafs { + static function noSubselectionAllowedMessage($field, $type) + { + return "Field \"$field\" of type \"$type\" must not have a sub selection."; + } + + static function requiredSubselectionMessage($field, $type) + { + return "Field \"$field\" of type \"$type\" must have a sub selection."; + } + public function __invoke(ValidationContext $context) { return [ @@ -20,13 +30,13 @@ class ScalarLeafs if (Type::isLeafType($type)) { if ($node->selectionSet) { return new Error( - Messages::noSubselectionAllowedMessage($node->name->value, $type), + self::noSubselectionAllowedMessage($node->name->value, $type), [$node->selectionSet] ); } } else if (!$node->selectionSet) { return new Error( - Messages::requiredSubselectionMessage($node->name->value, $type), + self::requiredSubselectionMessage($node->name->value, $type), [$node] ); } diff --git a/src/Validator/Rules/VariablesAreInputTypes.php b/src/Validator/Rules/VariablesAreInputTypes.php index 7eca699..faca0ce 100644 --- a/src/Validator/Rules/VariablesAreInputTypes.php +++ b/src/Validator/Rules/VariablesAreInputTypes.php @@ -4,40 +4,35 @@ namespace GraphQL\Validator\Rules; use GraphQL\Error; use GraphQL\Language\AST\Node; -use GraphQL\Language\AST\Type; use GraphQL\Language\AST\VariableDefinition; use GraphQL\Language\Printer; use GraphQL\Type\Definition\InputType; +use GraphQL\Type\Definition\Type; use GraphQL\Utils; -use GraphQL\Validator\Messages; use GraphQL\Validator\ValidationContext; class VariablesAreInputTypes { + static function nonInputTypeOnVarMessage($variableName, $typeName) + { + return "Variable \"\$$variableName\" cannot be non-input type \"$typeName\"."; + } + public function __invoke(ValidationContext $context) { return [ Node::VARIABLE_DEFINITION => function(VariableDefinition $node) use ($context) { - $typeName = $this->getTypeASTName($node->type); - $type = $context->getSchema()->getType($typeName); + $type = Utils\TypeInfo::typeFromAST($context->getSchema(), $node->type); - if (!($type instanceof InputType)) { + // If the variable type is not an input type, return an error. + if ($type && !Type::isInputType($type)) { $variableName = $node->variable->name->value; return new Error( - Messages::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)), - [$node->type] + self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)), + [ $node->type ] ); } } ]; } - - private function getTypeASTName(Type $typeAST) - { - if ($typeAST->kind === Node::NAME) { - return $typeAST->value; - } - Utils::invariant($typeAST->type, 'Must be wrapping type'); - return $this->getTypeASTName($typeAST->type); - } } diff --git a/src/Validator/ValidationContext.php b/src/Validator/ValidationContext.php index eaac972..27d56f1 100644 --- a/src/Validator/ValidationContext.php +++ b/src/Validator/ValidationContext.php @@ -113,4 +113,14 @@ class ValidationContext { return $this->_typeInfo->getFieldDef(); } + + function getDirective() + { + return $this->_typeInfo->getDirective(); + } + + function getArgument() + { + return $this->_typeInfo->getArgument(); + } } diff --git a/tests/Executor/AbstractTest.php b/tests/Executor/AbstractTest.php new file mode 100644 index 0000000..378810f --- /dev/null +++ b/tests/Executor/AbstractTest.php @@ -0,0 +1,244 @@ + 'Pet', + 'fields' => [ + 'name' => ['type' => Type::string()] + ] + ]); + + // Added to interface type when defined + $dogType = new ObjectType([ + 'name' => 'Dog', + 'interfaces' => [$petType], + 'isTypeOf' => function($obj) { return $obj instanceof Dog; }, + 'fields' => [ + 'name' => ['type' => Type::string()], + 'woofs' => ['type' => Type::boolean()] + ] + ]); + + $catType = new ObjectType([ + 'name' => 'Cat', + 'interfaces' => [$petType], + 'isTypeOf' => function ($obj) { + return $obj instanceof Cat; + }, + 'fields' => [ + 'name' => ['type' => Type::string()], + 'meows' => ['type' => Type::boolean()], + ] + ]); + + $schema = new Schema( + new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'pets' => [ + 'type' => Type::listOf($petType), + 'resolve' => function () { + return [new Dog('Odie', true), new Cat('Garfield', false)]; + } + ] + ] + ]) + ); + + $query = '{ + pets { + name + ... on Dog { + woofs + } + ... on Cat { + meows + } + } + }'; + + $expected = new ExecutionResult([ + 'pets' => [ + ['name' => 'Odie', 'woofs' => true], + ['name' => 'Garfield', 'meows' => false] + ] + ]); + + $this->assertEquals($expected, Executor::execute($schema, Parser::parse($query))); + } + + public function testIsTypeOfUsedToResolveRuntimeTypeForUnion() + { + // isTypeOf used to resolve runtime type for Union + + $dogType = new ObjectType([ + 'name' => 'Dog', + 'isTypeOf' => function($obj) { return $obj instanceof Dog; }, + 'fields' => [ + 'name' => ['type' => Type::string()], + 'woofs' => ['type' => Type::boolean()] + ] + ]); + + $catType = new ObjectType([ + 'name' => 'Cat', + 'isTypeOf' => function ($obj) { + return $obj instanceof Cat; + }, + 'fields' => [ + 'name' => ['type' => Type::string()], + 'meows' => ['type' => Type::boolean()], + ] + ]); + + $petType = new UnionType([ + 'name' => 'Pet', + 'types' => [$dogType, $catType] + ]); + + $schema = new Schema(new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'pets' => [ + 'type' => Type::listOf($petType), + 'resolve' => function() { + return [ new Dog('Odie', true), new Cat('Garfield', false) ]; + } + ] + ] + ])); + + $query = '{ + pets { + name + ... on Dog { + woofs + } + ... on Cat { + meows + } + } + }'; + + $expected = new ExecutionResult([ + 'pets' => [ + ['name' => 'Odie', 'woofs' => true], + ['name' => 'Garfield', 'meows' => false] + ] + ]); + + $this->assertEquals($expected, Executor::execute($schema, Parser::parse($query))); + } + + function testResolveTypeOnInterfaceYieldsUsefulError() + { + $DogType = null; + $CatType = null; + $HumanType = null; + + $PetType = new InterfaceType([ + 'name' => 'Pet', + 'resolveType' => function ($obj) use (&$DogType, &$CatType, &$HumanType) { + if ($obj instanceof Dog) { + return $DogType; + } + if ($obj instanceof Cat) { + return $CatType; + } + if ($obj instanceof Human) { + return $HumanType; + } + return null; + }, + 'fields' => [ + 'name' => ['type' => Type::string()] + ] + ]); + + $HumanType = new ObjectType([ + 'name' => 'Human', + 'fields' => [ + 'name' => ['type' => Type::string()], + ] + ]); + + $DogType = new ObjectType([ + 'name' => 'Dog', + 'interfaces' => [$PetType], + 'fields' => [ + 'name' => ['type' => Type::string()], + 'woofs' => ['type' => Type::boolean()], + ] + ]); + + $CatType = new ObjectType([ + 'name' => 'Cat', + 'interfaces' => [$PetType], + 'fields' => [ + 'name' => ['type' => Type::string()], + 'meows' => ['type' => Type::boolean()], + ] + ]); + + $schema = new Schema(new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'pets' => [ + 'type' => Type::listOf($PetType), + 'resolve' => function () { + return [ + new Dog('Odie', true), + new Cat('Garfield', false), + new Human('Jon') + ]; + } + ] + ] + ])); + + + $query = '{ + pets { + name + ... on Dog { + woofs + } + ... on Cat { + meows + } + } + }'; + + $expected = [ + 'data' => [ + 'pets' => [ + ['name' => 'Odie', 'woofs' => true], + ['name' => 'Garfield', 'meows' => false], + null + ] + ], + 'errors' => [ + [ 'message' => 'Runtime Object type "Human" is not a possible type for "Pet".' ] + ] + ]; + + $this->assertEquals($expected, Executor::execute($schema, Parser::parse($query))->toArray()); + } + +} diff --git a/tests/Executor/DirectivesTest.php b/tests/Executor/DirectivesTest.php index 7c1df7b..78bc564 100644 --- a/tests/Executor/DirectivesTest.php +++ b/tests/Executor/DirectivesTest.php @@ -19,16 +19,16 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase public function testWorksOnScalars() { // if true includes scalar - $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b @if:true }')); + $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b @include(if: true) }')); // if false omits on scalar - $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @if:false }')); + $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @include(if: false) }')); // unless false includes scalar - $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b @unless:false }')); + $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b @skip(if: false) }')); // unless true omits scalar - $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @unless:true }')); + $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @skip(if: true) }')); } public function testWorksOnFragmentSpreads() @@ -37,7 +37,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase $q = ' query Q { a - ...Frag @if:false + ...Frag @include(if: false) } fragment Frag on TestType { b @@ -49,7 +49,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase $q = ' query Q { a - ...Frag @if:true + ...Frag @include(if: true) } fragment Frag on TestType { b @@ -61,7 +61,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase $q = ' query Q { a - ...Frag @unless:false + ...Frag @skip(if: false) } fragment Frag on TestType { b @@ -73,7 +73,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase $q = ' query Q { a - ...Frag @unless:true + ...Frag @skip(if: true) } fragment Frag on TestType { b @@ -88,7 +88,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase $q = ' query Q { a - ... on TestType @if:false { + ... on TestType @include(if: false) { b } } @@ -102,7 +102,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase $q = ' query Q { a - ... on TestType @if:true { + ... on TestType @include(if: true) { b } } @@ -116,7 +116,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase $q = ' query Q { a - ... on TestType @unless:false { + ... on TestType @skip(if: false) { b } } @@ -130,7 +130,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase $q = ' query Q { a - ... on TestType @unless:true { + ... on TestType @skip(if: true) { b } } @@ -149,7 +149,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase a ...Frag } - fragment Frag on TestType @if:false { + fragment Frag on TestType @include(if: false) { b } '; @@ -161,7 +161,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase a ...Frag } - fragment Frag on TestType @if:true { + fragment Frag on TestType @include(if: true) { b } '; @@ -173,7 +173,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase a ...Frag } - fragment Frag on TestType @unless:false { + fragment Frag on TestType @skip(if: false) { b } '; @@ -185,7 +185,7 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase a ...Frag } - fragment Frag on TestType @unless:true { + fragment Frag on TestType @skip(if: true) { b } '; @@ -213,13 +213,13 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase private static function getData() { return self::$data ?: (self::$data = [ - 'a' => function() { return 'a'; }, - 'b' => function() { return 'b'; } + 'a' => 'a', + 'b' => 'b' ]); } private function executeTestQuery($doc) { - return Executor::execute(self::getSchema(), self::getData(), Parser::parse($doc)); + return Executor::execute(self::getSchema(), Parser::parse($doc), self::getData())->toArray(); } } diff --git a/tests/Executor/ExecutorSchemaTest.php b/tests/Executor/ExecutorSchemaTest.php index 6240dfe..8b23ae5 100644 --- a/tests/Executor/ExecutorSchemaTest.php +++ b/tests/Executor/ExecutorSchemaTest.php @@ -168,7 +168,7 @@ class ExecutorSchemaTest extends \PHPUnit_Framework_TestCase ] ]; - $this->assertEquals($expected, Executor::execute($BlogSchema, null, Parser::parse($request), '', [])); + $this->assertEquals($expected, Executor::execute($BlogSchema, Parser::parse($request))->toArray()); } private function article($id) diff --git a/tests/Executor/ExecutorTest.php b/tests/Executor/ExecutorTest.php index 29ddf59..76d9784 100644 --- a/tests/Executor/ExecutorTest.php +++ b/tests/Executor/ExecutorTest.php @@ -1,6 +1,7 @@ assertEquals($expected, Executor::execute($schema, $data, $ast, 'Example', ['size' => 100])); + $this->assertEquals($expected, Executor::execute($schema, $ast, $data, ['size' => 100], 'Example')->toArray()); } public function testMergesParallelFragments() @@ -187,11 +188,12 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ]; - $this->assertEquals($expected, Executor::execute($schema, null, $ast)); + $this->assertEquals($expected, Executor::execute($schema, $ast)->toArray()); } public function testThreadsContextCorrectly() { + // threads context correctly $doc = 'query Example { a }'; $gotHere = false; @@ -214,7 +216,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ])); - Executor::execute($schema, $data, $ast, 'Example', []); + Executor::execute($schema, $ast, $data, [], 'Example'); $this->assertEquals(true, $gotHere); } @@ -246,7 +248,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ] ])); - Executor::execute($schema, null, $docAst, 'Example', []); + Executor::execute($schema, $docAst, null, [], 'Example'); $this->assertSame($gotHere, true); } @@ -255,6 +257,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase $doc = '{ sync, syncError, + syncRawError, async, asyncReject, asyncError @@ -265,9 +268,11 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase return 'sync'; }, 'syncError' => function () { - throw new \Exception('Error getting syncError'); + throw new Error('Error getting syncError'); + }, + 'syncRawError' => function() { + throw new \Exception('Error getting syncRawError'); }, - // Following are inherited from JS reference implementation, but make no sense in this PHP impl // leaving them just to simplify migrations from newer js versions 'async' => function() { @@ -287,6 +292,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase 'fields' => [ 'sync' => ['type' => Type::string()], 'syncError' => ['type' => Type::string()], + 'syncRawError' => [ 'type' => Type::string() ], 'async' => ['type' => Type::string()], 'asyncReject' => ['type' => Type::string() ], 'asyncError' => ['type' => Type::string()], @@ -297,20 +303,22 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase 'data' => [ 'sync' => 'sync', 'syncError' => null, + 'syncRawError' => null, 'async' => 'async', 'asyncReject' => null, 'asyncError' => null, ], 'errors' => [ - new FormattedError('Error getting syncError', [new SourceLocation(3, 7)]), - new FormattedError('Error getting asyncReject', [new SourceLocation(5, 7)]), - new FormattedError('Error getting asyncError', [new SourceLocation(6, 7)]) + FormattedError::create('Error getting syncError', [new SourceLocation(3, 7)]), + FormattedError::create('Error getting syncRawError', [new SourceLocation(4, 7)]), + FormattedError::create('Error getting asyncReject', [new SourceLocation(6, 7)]), + FormattedError::create('Error getting asyncError', [new SourceLocation(7, 7)]) ] ]; - $result = Executor::execute($schema, $data, $docAst); + $result = Executor::execute($schema, $docAst, $data); - $this->assertEquals($expected, $result); + $this->assertEquals($expected, $result->toArray()); } public function testUsesTheInlineOperationIfNoOperationIsProvided() @@ -326,9 +334,9 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ])); - $ex = Executor::execute($schema, $data, $ast); + $ex = Executor::execute($schema, $ast, $data); - $this->assertEquals(['data' => ['a' => 'b']], $ex); + $this->assertEquals(['data' => ['a' => 'b']], $ex->toArray()); } public function testUsesTheOnlyOperationIfNoOperationIsProvided() @@ -343,8 +351,8 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ])); - $ex = Executor::execute($schema, $data, $ast); - $this->assertEquals(['data' => ['a' => 'b']], $ex); + $ex = Executor::execute($schema, $ast, $data); + $this->assertEquals(['data' => ['a' => 'b']], $ex->toArray()); } public function testThrowsIfNoOperationIsProvidedWithMultipleOperations() @@ -359,15 +367,12 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ])); - $ex = Executor::execute($schema, $data, $ast); - - $this->assertEquals( - [ - 'data' => null, - 'errors' => [new FormattedError('Must provide operation name if query contains multiple operations')] - ], - $ex - ); + try { + Executor::execute($schema, $ast, $data); + $this->fail('Expected exception is not thrown'); + } catch (Error $err) { + $this->assertEquals('Must provide operation name if query contains multiple operations.', $err->getMessage()); + } } public function testUsesTheQuerySchemaForQueries() @@ -390,8 +395,8 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ]) ); - $queryResult = Executor::execute($schema, $data, $ast, 'Q'); - $this->assertEquals(['data' => ['a' => 'b']], $queryResult); + $queryResult = Executor::execute($schema, $ast, $data, [], 'Q'); + $this->assertEquals(['data' => ['a' => 'b']], $queryResult->toArray()); } public function testUsesTheMutationSchemaForMutations() @@ -413,8 +418,8 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ]) ); - $mutationResult = Executor::execute($schema, $data, $ast, 'M'); - $this->assertEquals(['data' => ['c' => 'd']], $mutationResult); + $mutationResult = Executor::execute($schema, $ast, $data, [], 'M'); + $this->assertEquals(['data' => ['c' => 'd']], $mutationResult->toArray()); } public function testAvoidsRecursion() @@ -440,8 +445,8 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ])); - $queryResult = Executor::execute($schema, $data, $ast, 'Q'); - $this->assertEquals(['data' => ['a' => 'b']], $queryResult); + $queryResult = Executor::execute($schema, $ast, $data, [], 'Q'); + $this->assertEquals(['data' => ['a' => 'b']], $queryResult->toArray()); } public function testDoesNotIncludeIllegalFieldsInOutput() @@ -464,7 +469,106 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ]) ); - $mutationResult = Executor::execute($schema, null, $ast); - $this->assertEquals(['data' => []], $mutationResult); + $mutationResult = Executor::execute($schema, $ast); + $this->assertEquals(['data' => []], $mutationResult->toArray()); + } + + public function testDoesNotIncludeArgumentsThatWereNotSet() + { + $schema = new Schema( + new ObjectType([ + 'name' => 'Type', + 'fields' => [ + 'field' => [ + 'type' => Type::string(), + 'resolve' => function($data, $args) {return $args ? json_encode($args) : '';}, + 'args' => [ + 'a' => ['type' => Type::boolean()], + 'b' => ['type' => Type::boolean()], + 'c' => ['type' => Type::boolean()], + 'd' => ['type' => Type::int()], + 'e' => ['type' => Type::int()] + ] + ] + ] + ]) + ); + + $query = Parser::parse('{ field(a: true, c: false, e: 0) }'); + $result = Executor::execute($schema, $query); + $expected = [ + 'data' => [ + 'field' => '{"a":true,"c":false,"e":0}' + ] + ]; + + $this->assertEquals($expected, $result->toArray()); + +/* + var query = parse('{ field(a: true, c: false, e: 0) }'); + var result = await execute(schema, query); + + expect(result).to.deep.equal({ + data: { + field: '{"a":true,"c":false,"e":0}' + } + }); + }); + + it('fails when an isTypeOf check is not met', async () => { + class Special { + constructor(value) { + this.value = value; + } + } + + class NotSpecial { + constructor(value) { + this.value = value; + } + } + + var SpecialType = new GraphQLObjectType({ + name: 'SpecialType', + isTypeOf(obj) { + return obj instanceof Special; + }, + fields: { + value: { type: GraphQLString } + } + }); + + var schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + specials: { + type: new GraphQLList(SpecialType), + resolve: rootValue => rootValue.specials + } + } + }) + }); + + var query = parse('{ specials { value } }'); + var value = { + specials: [ new Special('foo'), new NotSpecial('bar') ] + }; + var result = await execute(schema, query, value); + + expect(result.data).to.deep.equal({ + specials: [ + { value: 'foo' }, + null + ] + }); + expect(result.errors).to.have.lengthOf(1); + expect(result.errors).to.containSubset([ + { message: + 'Expected value of type "SpecialType" but got: [object Object].', + locations: [ { line: 1, column: 3 } ] } + ]); + }); + */ } } diff --git a/tests/Executor/ListsTest.php b/tests/Executor/ListsTest.php index 36b0294..25a9bd1 100644 --- a/tests/Executor/ListsTest.php +++ b/tests/Executor/ListsTest.php @@ -1,6 +1,7 @@ ['nest' => ['list' => [1,2]]]]; - $this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); } public function testHandlesListsOfNonNullsWhenTheyReturnNonNullValues() @@ -47,7 +48,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase ] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); } public function testHandlesNonNullListsOfWhenTheyReturnNonNullValues() @@ -69,7 +70,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase ] ] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); } public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNonNullValues() @@ -91,7 +92,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase ] ] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); } public function testHandlesListsWhenTheyReturnNullAsAValue() @@ -113,7 +114,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase ] ] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); } public function testHandlesListsOfNonNullsWhenTheyReturnNullAsAValue() @@ -135,13 +136,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase ] ], 'errors' => [ - new FormattedError( + FormattedError::create( 'Cannot return null for non-nullable type.', [new SourceLocation(4, 11)] ) ] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); } public function testHandlesNonNullListsOfWhenTheyReturnNullAsAValue() @@ -160,7 +161,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase 'nest' => ['nonNullListContainsNull' => [1, null, 2]] ] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); } public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNullAsAValue() @@ -180,13 +181,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase 'nest' => null ], 'errors' => [ - new FormattedError( + FormattedError::create( 'Cannot return null for non-nullable type.', [new SourceLocation(4, 11)] ) ] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); } public function testHandlesListsWhenTheyReturnNull() @@ -208,7 +209,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase ] ] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); } public function testHandlesListsOfNonNullsWhenTheyReturnNull() @@ -230,7 +231,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase ] ] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); } public function testHandlesNonNullListsOfWhenTheyReturnNull() @@ -250,13 +251,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase 'nest' => null, ], 'errors' => [ - new FormattedError( + FormattedError::create( 'Cannot return null for non-nullable type.', [new SourceLocation(4, 11)] ) ] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); } public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNull() @@ -276,13 +277,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase 'nest' => null ], 'errors' => [ - new FormattedError( + FormattedError::create( 'Cannot return null for non-nullable type.', [new SourceLocation(4, 11)] ) ] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $this->data(), $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); } diff --git a/tests/Executor/MutationsTest.php b/tests/Executor/MutationsTest.php index 35e5653..768d4a3 100644 --- a/tests/Executor/MutationsTest.php +++ b/tests/Executor/MutationsTest.php @@ -31,7 +31,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase } }'; $ast = Parser::parse($doc); - $mutationResult = Executor::execute($this->schema(), new Root(6), $ast, 'M'); + $mutationResult = Executor::execute($this->schema(), $ast, new Root(6), null, 'M'); $expected = [ 'data' => [ 'first' => [ @@ -51,7 +51,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase ] ] ]; - $this->assertEquals($mutationResult, $expected); + $this->assertEquals($expected, $mutationResult->toArray()); } public function testEvaluatesMutationsCorrectlyInThePresenseOfAFailedMutation() @@ -77,7 +77,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase } }'; $ast = Parser::parse($doc); - $mutationResult = Executor::execute($this->schema(), new Root(6), $ast, 'M'); + $mutationResult = Executor::execute($this->schema(), $ast, new Root(6), null, 'M'); $expected = [ 'data' => [ 'first' => [ @@ -96,17 +96,17 @@ class MutationsTest extends \PHPUnit_Framework_TestCase 'sixth' => null, ], 'errors' => [ - new FormattedError( + FormattedError::create( 'Cannot change the number', [new SourceLocation(8, 7)] ), - new FormattedError( + FormattedError::create( 'Cannot change the number', [new SourceLocation(17, 7)] ) ] ]; - $this->assertEquals($expected, $mutationResult); + $this->assertEquals($expected, $mutationResult->toArray()); } private function schema() diff --git a/tests/Executor/NonNullTest.php b/tests/Executor/NonNullTest.php index 76e00cc..4cd9fe6 100644 --- a/tests/Executor/NonNullTest.php +++ b/tests/Executor/NonNullTest.php @@ -86,13 +86,13 @@ class NonNullTest extends \PHPUnit_Framework_TestCase 'sync' => null, ], 'errors' => [ - new FormattedError( + FormattedError::create( $this->syncError->message, [new SourceLocation(3, 9)] ) ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->throwingData, $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray()); } public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatThrowsSynchronously() @@ -113,10 +113,10 @@ class NonNullTest extends \PHPUnit_Framework_TestCase 'nest' => null ], 'errors' => [ - new FormattedError($this->nonNullSyncError->message, [new SourceLocation(4, 11)]) + FormattedError::create($this->nonNullSyncError->message, [new SourceLocation(4, 11)]) ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->throwingData, $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray()); } public function testNullsAComplexTreeOfNullableFieldsThatThrow() @@ -144,11 +144,11 @@ class NonNullTest extends \PHPUnit_Framework_TestCase ] ], 'errors' => [ - new FormattedError($this->syncError->message, [new SourceLocation(4, 11)]), - new FormattedError($this->syncError->message, [new SourceLocation(6, 13)]), + FormattedError::create($this->syncError->message, [new SourceLocation(4, 11)]), + FormattedError::create($this->syncError->message, [new SourceLocation(6, 13)]), ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->throwingData, $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray()); } public function testNullsANullableFieldThatSynchronouslyReturnsNull() @@ -166,7 +166,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase 'sync' => null, ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->nullingData, $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray()); } public function test4() @@ -187,10 +187,10 @@ class NonNullTest extends \PHPUnit_Framework_TestCase 'nest' => null ], 'errors' => [ - new FormattedError('Cannot return null for non-nullable type.', [new SourceLocation(4, 11)]) + FormattedError::create('Cannot return null for non-nullable type.', [new SourceLocation(4, 11)]) ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->nullingData, $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray()); } public function test5() @@ -226,7 +226,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase ], ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->nullingData, $ast, 'Q', [])); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray()); } public function testNullsTheTopLevelIfSyncNonNullableFieldThrows() @@ -238,10 +238,10 @@ class NonNullTest extends \PHPUnit_Framework_TestCase $expected = [ 'data' => null, 'errors' => [ - new FormattedError($this->nonNullSyncError->message, [new SourceLocation(2, 17)]) + FormattedError::create($this->nonNullSyncError->message, [new SourceLocation(2, 17)]) ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->throwingData, Parser::parse($doc))); + $this->assertEquals($expected, Executor::execute($this->schema, Parser::parse($doc), $this->throwingData)->toArray()); } public function testNullsTheTopLevelIfSyncNonNullableFieldReturnsNull() @@ -254,9 +254,9 @@ class NonNullTest extends \PHPUnit_Framework_TestCase $expected = [ 'data' => null, 'errors' => [ - new FormattedError('Cannot return null for non-nullable type.', [new SourceLocation(2, 17)]), + FormattedError::create('Cannot return null for non-nullable type.', [new SourceLocation(2, 17)]), ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->nullingData, Parser::parse($doc))); + $this->assertEquals($expected, Executor::execute($this->schema, Parser::parse($doc), $this->nullingData)->toArray()); } } diff --git a/tests/Executor/TestClasses.php b/tests/Executor/TestClasses.php new file mode 100644 index 0000000..c1fe6ba --- /dev/null +++ b/tests/Executor/TestClasses.php @@ -0,0 +1,78 @@ +name = $name; + $this->woofs = $woofs; + } +} + +class Cat +{ + function __construct($name, $meows) + { + $this->name = $name; + $this->meows = $meows; + } +} + +class Human +{ + function __construct($name) + { + $this->name = $name; + } +} + +class Person +{ + public $name; + public $pets; + public $friends; + + function __construct($name, $pets = null, $friends = null) + { + $this->name = $name; + $this->pets = $pets; + $this->friends = $friends; + } +} + +class ComplexScalar extends ScalarType +{ + public static function create() + { + return new self(); + } + + public $name = 'ComplexScalar'; + + public function serialize($value) + { + if ($value === 'DeserializedValue') { + return 'SerializedValue'; + } + return null; + } + + public function parseValue($value) + { + if ($value === 'SerializedValue') { + return 'DeserializedValue'; + } + return null; + } + + public function parseLiteral($valueAST) + { + if ($valueAST->value === 'SerializedValue') { + return 'DeserializedValue'; + } + return null; + } +} diff --git a/tests/Executor/UnionInterfaceTest.php b/tests/Executor/UnionInterfaceTest.php index e04a9ae..6a7f03b 100644 --- a/tests/Executor/UnionInterfaceTest.php +++ b/tests/Executor/UnionInterfaceTest.php @@ -1,6 +1,8 @@ [$NamedType], 'fields' => [ 'name' => ['type' => Type::string()], - 'barks' => ['type' => Type::boolean()] + 'woofs' => ['type' => Type::boolean()] ], 'isTypeOf' => function ($value) { return $value instanceof Dog; @@ -143,7 +145,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase ] ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, null, $ast)); + $this->assertEquals($expected, Executor::execute($this->schema, $ast)->toArray()); } public function testExecutesUsingUnionTypes() @@ -156,7 +158,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase pets { __typename name - barks + woofs meows } } @@ -167,12 +169,12 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase 'name' => 'John', 'pets' => [ ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false], - ['__typename' => 'Dog', 'name' => 'Odie', 'barks' => true] + ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true] ] ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); } public function testExecutesUnionTypesWithInlineFragments() @@ -186,7 +188,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase __typename ... on Dog { name - barks + woofs } ... on Cat { name @@ -201,12 +203,12 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase 'name' => 'John', 'pets' => [ ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false], - ['__typename' => 'Dog', 'name' => 'Odie', 'barks' => true] + ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true] ] ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); } public function testExecutesUsingInterfaceTypes() @@ -219,7 +221,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase friends { __typename name - barks + woofs meows } } @@ -230,12 +232,12 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase 'name' => 'John', 'friends' => [ ['__typename' => 'Person', 'name' => 'Liz'], - ['__typename' => 'Dog', 'name' => 'Odie', 'barks' => true] + ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true] ] ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); } public function testExecutesInterfaceTypesWithInlineFragments() @@ -249,7 +251,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase __typename name ... on Dog { - barks + woofs } ... on Cat { meows @@ -263,12 +265,12 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase 'name' => 'John', 'friends' => [ ['__typename' => 'Person', 'name' => 'Liz'], - ['__typename' => 'Dog', 'name' => 'Odie', 'barks' => true] + ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true] ] ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); } public function testAllowsFragmentConditionsToBeAbstractTypes() @@ -285,7 +287,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase __typename ... on Dog { name - barks + woofs } ... on Cat { name @@ -297,7 +299,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase __typename name ... on Dog { - barks + woofs } ... on Cat { meows @@ -311,54 +313,15 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase 'name' => 'John', 'pets' => [ ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false], - ['__typename' => 'Dog', 'name' => 'Odie', 'barks' => true] + ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true] ], 'friends' => [ ['__typename' => 'Person', 'name' => 'Liz'], - ['__typename' => 'Dog', 'name' => 'Odie', 'barks' => true] + ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true] ] ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); - } -} - - -class Dog -{ - public $name; - public $barks; - - function __construct($name, $barks) - { - $this->name = $name; - $this->barks = $barks; - } -} - -class Cat -{ - public $name; - public $meows; - - function __construct($name, $meows) - { - $this->name = $name; - $this->meows = $meows; - } -} - -class Person -{ - public $name; - public $pets; - public $friends; - - function __construct($name, $pets = null, $friends = null) - { - $this->name = $name; - $this->pets = $pets; - $this->friends = $friends; + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); } } diff --git a/tests/Executor/InputObjectTest.php b/tests/Executor/VariablesTest.php similarity index 59% rename from tests/Executor/InputObjectTest.php rename to tests/Executor/VariablesTest.php index 4f3ed19..6cde735 100644 --- a/tests/Executor/InputObjectTest.php +++ b/tests/Executor/VariablesTest.php @@ -1,17 +1,21 @@ '{"a":"foo","b":["bar"],"c":"baz"}' ] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); - // properly coerces single value to array: + // properly parses single value to list: $doc = ' { fieldWithObjectInput(input: {a: "foo", b: "bar", c: "baz"}) @@ -40,7 +44,23 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase $ast = Parser::parse($doc); $expected = ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); + } + + public function testDoesNotUseIncorrectValue() + { + $doc = ' + { + fieldWithObjectInput(input: ["foo", "bar", "baz"]) + } + '; + $ast = Parser::parse($doc); + $result = Executor::execute($this->schema(), $ast)->toArray(); + + $expected = [ + 'data' => ['fieldWithObjectInput' => null] + ]; + $this->assertEquals($expected, $result); } public function testUsingVariables() @@ -57,46 +77,100 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase $this->assertEquals( ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']], - Executor::execute($schema, null, $ast, null, $params) + Executor::execute($schema, $ast, null, $params)->toArray() ); - // properly coerces single value to array: + // uses default value when not provided: + $withDefaultsAST = Parser::parse(' + query q($input: TestInputObject = {a: "foo", b: ["bar"], c: "baz"}) { + fieldWithObjectInput(input: $input) + } + '); + + $result = Executor::execute($this->schema(), $withDefaultsAST)->toArray(); + $expected = [ + 'data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}'] + ]; + $this->assertEquals($expected, $result); + + + // properly parses single value to array: $params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']]; $this->assertEquals( ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']], - Executor::execute($schema, null, $ast, null, $params) + Executor::execute($schema, $ast, null, $params)->toArray() ); + // executes with complex scalar input: + $params = [ 'input' => [ 'c' => 'foo', 'd' => 'SerializedValue' ] ]; + $result = Executor::execute($schema, $ast, null, $params)->toArray(); + $expected = [ + 'data' => [ + 'fieldWithObjectInput' => '{"c":"foo","d":"DeserializedValue"}' + ] + ]; + $this->assertEquals($expected, $result); + // errors on null for nested non-null: $params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => null]]; - $expected = [ - 'data' => null, - 'errors' => [ - new FormattedError( - 'Variable $input expected value of type ' . - 'TestInputObject but got: ' . - '{"a":"foo","b":"bar","c":null}.', - [new SourceLocation(2, 17)] - ) - ] - ]; + $expected = FormattedError::create( + 'Variable $input expected value of type ' . + 'TestInputObject but got: ' . + '{"a":"foo","b":"bar","c":null}.', + [new SourceLocation(2, 17)] + ); - $this->assertEquals($expected, Executor::execute($schema, null, $ast, null, $params)); + try { + Executor::execute($schema, $ast, null, $params); + $this->fail('Expected exception not thrown'); + } catch (Error $err) { + $this->assertEquals($expected, Error::formatError($err)); + } + + // errors on incorrect type: + $params = [ 'input' => 'foo bar' ]; + + try { + Executor::execute($schema, $ast, null, $params); + $this->fail('Expected exception not thrown'); + } catch (Error $error) { + $expected = FormattedError::create( + 'Variable $input expected value of type TestInputObject but got: "foo bar".', + [new SourceLocation(2, 17)] + ); + $this->assertEquals($expected, Error::formatError($error)); + } // errors on omission of nested non-null: $params = ['input' => ['a' => 'foo', 'b' => 'bar']]; - $expected = [ - 'data' => null, - 'errors' => [ - new FormattedError( - 'Variable $input expected value of type ' . - 'TestInputObject but got: {"a":"foo","b":"bar"}.', - [new SourceLocation(2, 17)] - ) - ] - ]; - $this->assertEquals($expected, Executor::execute($schema, null, $ast, null, $params)); + try { + Executor::execute($schema, $ast, null, $params); + $this->fail('Expected exception not thrown'); + } catch (Error $e) { + $expected = FormattedError::create( + 'Variable $input expected value of type ' . + 'TestInputObject but got: {"a":"foo","b":"bar"}.', + [new SourceLocation(2, 17)] + ); + + $this->assertEquals($expected, Error::formatError($e)); + } + + // errors on addition of unknown input field + $params = ['input' => [ 'a' => 'foo', 'b' => 'bar', 'c' => 'baz', 'd' => 'dog' ]]; + + try { + Executor::execute($schema, $ast, null, $params); + $this->fail('Expected exception not thrown'); + } catch (Error $e) { + $expected = FormattedError::create( + 'Variable $input expected value of type TestInputObject but ' . + 'got: {"a":"foo","b":"bar","c":"baz","d":"dog"}.', + [new SourceLocation(2, 17)] + ); + $this->assertEquals($expected, Error::formatError($e)); + } } @@ -110,10 +184,10 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = [ - 'data' => ['fieldWithNullableStringInput' => 'null'] + 'data' => ['fieldWithNullableStringInput' => null] ]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); } public function testAllowsNullableInputsToBeOmittedInAVariable() @@ -124,9 +198,9 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase } '; $ast = Parser::parse($doc); - $expected = ['data' => ['fieldWithNullableStringInput' => 'null']]; + $expected = ['data' => ['fieldWithNullableStringInput' => null]]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); } public function testAllowsNullableInputsToBeOmittedInAnUnlistedVariable() @@ -137,8 +211,8 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase } '; $ast = Parser::parse($doc); - $expected = ['data' => ['fieldWithNullableStringInput' => 'null']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); + $expected = ['data' => ['fieldWithNullableStringInput' => null]]; + $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); } public function testAllowsNullableInputsToBeSetToNullInAVariable() @@ -149,21 +223,9 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase } '; $ast = Parser::parse($doc); - $expected = ['data' => ['fieldWithNullableStringInput' => 'null']]; + $expected = ['data' => ['fieldWithNullableStringInput' => null]]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['value' => null])); - } - - public function testAllowsNullableInputsToBeSetToNullDirectly() - { - $doc = ' - { - fieldWithNullableStringInput(input: null) - } - '; - $ast = Parser::parse($doc); - $expected = ['data' => ['fieldWithNullableStringInput' => 'null']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => null])->toArray()); } public function testAllowsNullableInputsToBeSetToAValueInAVariable() @@ -175,7 +237,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['fieldWithNullableStringInput' => '"a"']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['value' => 'a'])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => 'a'])->toArray()); } public function testAllowsNullableInputsToBeSetToAValueDirectly() @@ -187,7 +249,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['fieldWithNullableStringInput' => '"a"']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); } @@ -201,16 +263,16 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase } '; $ast = Parser::parse($doc); - $expected = [ - 'data' => null, - 'errors' => [ - new FormattedError( - 'Variable $value expected value of type String! but got: null.', - [new SourceLocation(2, 31)] - ) - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); + try { + Executor::execute($this->schema(), $ast); + $this->fail('Expected exception not thrown'); + } catch (Error $e) { + $expected = FormattedError::create( + 'Variable $value expected value of type String! but got: null.', + [new SourceLocation(2, 31)] + ); + $this->assertEquals($expected, Error::formatError($e)); + } } public function testDoesNotAllowNonNullableInputsToBeSetToNullInAVariable() @@ -222,16 +284,17 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase } '; $ast = Parser::parse($doc); - $expected = [ - 'data' => null, - 'errors' => [ - new FormattedError( - 'Variable $value expected value of type String! but got: null.', - [new SourceLocation(2, 31)] - ) - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['value' => null])); + + try { + Executor::execute($this->schema(), $ast, null, ['value' => null]); + $this->fail('Expected exception not thrown'); + } catch (Error $e) { + $expected = FormattedError::create( + 'Variable $value expected value of type String! but got: null.', + [new SourceLocation(2, 31)] + ); + $this->assertEquals($expected, Error::formatError($e)); + } } public function testAllowsNonNullableInputsToBeSetToAValueInAVariable() @@ -243,7 +306,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['fieldWithNonNullableStringInput' => '"a"']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['value' => 'a'])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => 'a'])->toArray()); } public function testAllowsNonNullableInputsToBeSetToAValueDirectly() @@ -256,7 +319,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase $ast = Parser::parse($doc); $expected = ['data' => ['fieldWithNonNullableStringInput' => '"a"']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); } public function testPassesAlongNullForNonNullableInputsIfExplcitlySetInTheQuery() @@ -267,8 +330,8 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase } '; $ast = Parser::parse($doc); - $expected = ['data' => ['fieldWithNonNullableStringInput' => 'null']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast)); + $expected = ['data' => ['fieldWithNonNullableStringInput' => null]]; + $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); } // Handles lists and nullability @@ -280,9 +343,9 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase } '; $ast = Parser::parse($doc); - $expected = ['data' => ['list' => 'null']]; + $expected = ['data' => ['list' => null]]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => null])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray()); } public function testAllowsListsToContainValues() @@ -294,7 +357,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['list' => '["A"]']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => ['A']])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A']])->toArray()); } public function testAllowsListsToContainNull() @@ -306,7 +369,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['list' => '["A",null,"B"]']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => ['A',null,'B']])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A',null,'B']])->toArray()); } public function testDoesNotAllowNonNullListsToBeNull() @@ -317,17 +380,17 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase } '; $ast = Parser::parse($doc); - $expected = [ - 'data' => null, - 'errors' => [ - new FormattedError( - 'Variable $input expected value of type [String]! but got: null.', - [new SourceLocation(2, 17)] - ) - ] - ]; + $expected = FormattedError::create( + 'Variable $input expected value of type [String]! but got: null.', + [new SourceLocation(2, 17)] + ); - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => null])); + try { + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray()); + $this->fail('Expected exception not thrown'); + } catch (Error $e) { + $this->assertEquals($expected, Error::formatError($e)); + } } public function testAllowsNonNullListsToContainValues() @@ -339,7 +402,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['nnList' => '["A"]']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => 'A'])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => 'A'])->toArray()); } public function testAllowsNonNullListsToContainNull() @@ -352,7 +415,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase $ast = Parser::parse($doc); $expected = ['data' => ['nnList' => '["A",null,"B"]']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => ['A',null,'B']])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A',null,'B']])->toArray()); } public function testAllowsListsOfNonNullsToBeNull() @@ -363,8 +426,8 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase } '; $ast = Parser::parse($doc); - $expected = ['data' => ['listNN' => 'null']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => null])); + $expected = ['data' => ['listNN' => null]]; + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray()); } public function testAllowsListsOfNonNullsToContainValues() @@ -377,7 +440,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase $ast = Parser::parse($doc); $expected = ['data' => ['listNN' => '["A"]']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => 'A'])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => 'A'])->toArray()); } public function testDoesNotAllowListsOfNonNullsToContainNull() @@ -388,16 +451,17 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase } '; $ast = Parser::parse($doc); - $expected = [ - 'data' => null, - 'errors' => [ - new FormattedError( - 'Variable $input expected value of type [String!] but got: ["A",null,"B"].', - [new SourceLocation(2, 17)] - ) - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => ['A', null, 'B']])); + $expected = FormattedError::create( + 'Variable $input expected value of type [String!] but got: ["A",null,"B"].', + [new SourceLocation(2, 17)] + ); + + try { + Executor::execute($this->schema(), $ast, null, ['input' => ['A', null, 'B']]); + $this->fail('Expected exception not thrown'); + } catch (Error $e) { + $this->assertEquals($expected, Error::formatError($e)); + } } public function testDoesNotAllowNonNullListsOfNonNullsToBeNull() @@ -408,16 +472,15 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase } '; $ast = Parser::parse($doc); - $expected = [ - 'data' => null, - 'errors' => [ - new FormattedError( - 'Variable $input expected value of type [String!]! but got: null.', - [new SourceLocation(2, 17)] - ) - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => null])); + $expected = FormattedError::create( + 'Variable $input expected value of type [String!]! but got: null.', + [new SourceLocation(2, 17)] + ); + try { + Executor::execute($this->schema(), $ast, null, ['input' => null]); + } catch (Error $e) { + $this->assertEquals($expected, Error::formatError($e)); + } } public function testAllowsNonNullListsOfNonNullsToContainValues() @@ -429,7 +492,7 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['nnListNN' => '["A"]']]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => ['A']])); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A']])->toArray()); } public function testDoesNotAllowNonNullListsOfNonNullsToContainNull() @@ -440,27 +503,29 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase } '; $ast = Parser::parse($doc); - $expected = [ - 'data' => null, - 'errors' => [ - new FormattedError( - 'Variable $input expected value of type [String!]! but got: ["A",null,"B"].', - [new SourceLocation(2, 17)] - ) - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), null, $ast, null, ['input' => ['A',null,'B']])); + $expected = FormattedError::create( + 'Variable $input expected value of type [String!]! but got: ["A",null,"B"].', + [new SourceLocation(2, 17)] + ); + try { + Executor::execute($this->schema(), $ast, null, ['input' => ['A', null, 'B']]); + } catch (Error $e) { + $this->assertEquals($expected, Error::formatError($e)); + } } public function schema() { + $ComplexScalarType = ComplexScalar::create(); + $TestInputObject = new InputObjectType([ 'name' => 'TestInputObject', 'fields' => [ 'a' => ['type' => Type::string()], 'b' => ['type' => Type::listOf(Type::string())], - 'c' => ['type' => Type::nonNull(Type::string())] + 'c' => ['type' => Type::nonNull(Type::string())], + 'd' => ['type' => $ComplexScalarType], ] ]); @@ -471,49 +536,56 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase 'type' => Type::string(), 'args' => ['input' => ['type' => $TestInputObject]], 'resolve' => function ($_, $args) { - return json_encode($args['input']); + return isset($args['input']) ? json_encode($args['input']) : null; } ], 'fieldWithNullableStringInput' => [ 'type' => Type::string(), 'args' => ['input' => ['type' => Type::string()]], 'resolve' => function ($_, $args) { - return json_encode($args['input']); + return isset($args['input']) ? json_encode($args['input']) : null; } ], 'fieldWithNonNullableStringInput' => [ 'type' => Type::string(), 'args' => ['input' => ['type' => Type::nonNull(Type::string())]], 'resolve' => function ($_, $args) { - return json_encode($args['input']); + return isset($args['input']) ? json_encode($args['input']) : null; + } + ], + 'fieldWithDefaultArgumentValue' => [ + 'type' => Type::string(), + 'args' => [ 'input' => [ 'type' => Type::string(), 'defaultValue' => 'Hello World' ]], + 'resolve' => function($_, $args) { + return isset($args['input']) ? json_encode($args['input']) : null; } ], 'list' => [ 'type' => Type::string(), 'args' => ['input' => ['type' => Type::listOf(Type::string())]], 'resolve' => function ($_, $args) { - return json_encode($args['input']); + return isset($args['input']) ? json_encode($args['input']) : null; } ], 'nnList' => [ 'type' => Type::string(), 'args' => ['input' => ['type' => Type::nonNull(Type::listOf(Type::string()))]], 'resolve' => function ($_, $args) { - return json_encode($args['input']); + return isset($args['input']) ? json_encode($args['input']) : null; } ], 'listNN' => [ 'type' => Type::string(), 'args' => ['input' => ['type' => Type::listOf(Type::nonNull(Type::string()))]], 'resolve' => function ($_, $args) { - return json_encode($args['input']); + return isset($args['input']) ? json_encode($args['input']) : null; } ], 'nnListNN' => [ 'type' => Type::string(), 'args' => ['input' => ['type' => Type::nonNull(Type::listOf(Type::nonNull(Type::string())))]], 'resolve' => function ($_, $args) { - return json_encode($args['input']); + return isset($args['input']) ? json_encode($args['input']) : null; } ], ] diff --git a/tests/StarWarsSchema.php b/tests/StarWarsSchema.php index cae03a9..07f400f 100644 --- a/tests/StarWarsSchema.php +++ b/tests/StarWarsSchema.php @@ -245,7 +245,7 @@ class StarWarsSchema ] ], 'resolve' => function ($root, $args) { - return StarWarsData::getHero($args['episode']); + return StarWarsData::getHero(isset($args['episode']) ? $args['episode'] : null); }, ], 'human' => [ diff --git a/tests/StarWarsValidationTest.php b/tests/StarWarsValidationTest.php index a2b76a0..357bb1c 100644 --- a/tests/StarWarsValidationTest.php +++ b/tests/StarWarsValidationTest.php @@ -28,8 +28,8 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase appearsIn } '; - $result = $this->validationResult($query); - $this->assertEquals(true, $result['isValid']); + $errors = $this->validationErrors($query); + $this->assertEquals(true, empty($errors)); } public function testThatNonExistentFieldsAreInvalid() @@ -42,7 +42,8 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase } } '; - $this->assertEquals(false, $this->validationResult($query)['isValid']); + $errors = $this->validationErrors($query); + $this->assertEquals(false, empty($errors)); } public function testRequiresFieldsOnObjects() @@ -52,7 +53,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase hero } '; - $this->assertEquals(false, $this->validationResult($query)['isValid']); + + $errors = $this->validationErrors($query); + $this->assertEquals(false, empty($errors)); } public function testDisallowsFieldsOnScalars() @@ -67,7 +70,8 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase } } '; - $this->assertEquals(false, $this->validationResult($query)['isValid']); + $errors = $this->validationErrors($query); + $this->assertEquals(false, empty($errors)); } public function testDisallowsObjectFieldsOnInterfaces() @@ -80,7 +84,8 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase } } '; - $this->assertEquals(false, $this->validationResult($query)['isValid']); + $errors = $this->validationErrors($query); + $this->assertEquals(false, empty($errors)); } public function testAllowsObjectFieldsInFragments() @@ -97,7 +102,8 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase primaryFunction } '; - $this->assertEquals(true, $this->validationResult($query)['isValid']); + $errors = $this->validationErrors($query); + $this->assertEquals(true, empty($errors)); } public function testAllowsObjectFieldsInInlineFragments() @@ -112,13 +118,14 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase } } '; - $this->assertEquals(true, $this->validationResult($query)['isValid']); + $errors = $this->validationErrors($query); + $this->assertEquals(true, empty($errors)); } /** * Helper function to test a query and the expected response. */ - private function validationResult($query) + private function validationErrors($query) { $ast = Parser::parse($query); return DocumentValidator::validate(StarWarsSchema::build(), $ast); diff --git a/tests/Type/DefinitionTest.php b/tests/Type/DefinitionTest.php index a9800da..a06d23a 100644 --- a/tests/Type/DefinitionTest.php +++ b/tests/Type/DefinitionTest.php @@ -6,7 +6,6 @@ use GraphQL\Type\Definition\Config; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; -use GraphQL\Type\Definition\IntType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; @@ -67,7 +66,10 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->objectType = new ObjectType(['name' => 'Object']); + $this->objectType = new ObjectType([ + 'name' => 'Object', + 'isTypeOf' => function() {return true;} + ]); $this->interfaceType = new InterfaceType(['name' => 'Interface']); $this->unionType = new UnionType(['name' => 'Union', 'types' => [$this->objectType]]); $this->enumType = new EnumType(['name' => 'Enum']); @@ -176,20 +178,83 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase $this->assertSame('writeArticle', $writeMutation->name); } + public function testIncludesNestedInputObjectInTheMap() + { + $nestedInputObject = new InputObjectType([ + 'name' => 'NestedInputObject', + 'fields' => ['value' => ['type' => Type::string()]] + ]); + $someInputObject = new InputObjectType([ + 'name' => 'SomeInputObject', + 'fields' => ['nested' => ['type' => $nestedInputObject]] + ]); + $someMutation = new ObjectType([ + 'name' => 'SomeMutation', + 'fields' => [ + 'mutateSomething' => [ + 'type' => $this->blogArticle, + 'args' => ['input' => ['type' => $someInputObject]] + ] + ] + ]); + + $schema = new Schema($this->blogQuery, $someMutation); + $this->assertSame($nestedInputObject, $schema->getType('NestedInputObject')); + } + public function testIncludesInterfaceSubtypesInTheTypeMap() { $someInterface = new InterfaceType([ 'name' => 'SomeInterface', - 'fields' => [] + 'fields' => [ + 'f' => ['type' => Type::int()] + ] ]); $someSubtype = new ObjectType([ 'name' => 'SomeSubtype', - 'fields' => [], - 'interfaces' => [$someInterface] + 'fields' => [ + 'f' => ['type' => Type::int()] + ], + 'interfaces' => [$someInterface], + 'isTypeOf' => function() {return true;} ]); - $schema = new Schema($someInterface); + $schema = new Schema(new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'iface' => ['type' => $someInterface] + ] + ])); + $this->assertSame($someSubtype, $schema->getType('SomeSubtype')); + } + + public function testIncludesInterfacesThunkSubtypesInTheTypeMap() + { + // includes interfaces' thunk subtypes in the type map + $someInterface = new InterfaceType([ + 'name' => 'SomeInterface', + 'fields' => [ + 'f' => ['type' => Type::int()] + ] + ]); + + $someSubtype = new ObjectType([ + 'name' => 'SomeSubtype', + 'fields' => [ + 'f' => ['type' => Type::int()] + ], + 'interfaces' => function() use ($someInterface) { return [$someInterface]; }, + 'isTypeOf' => function() {return true;} + ]); + + $schema = new Schema(new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'iface' => ['type' => $someInterface] + ] + ])); + $this->assertSame($someSubtype, $schema->getType('SomeSubtype')); } diff --git a/tests/Type/IntrospectionTest.php b/tests/Type/IntrospectionTest.php index 5af9b04..c9f28bc 100644 --- a/tests/Type/IntrospectionTest.php +++ b/tests/Type/IntrospectionTest.php @@ -9,7 +9,7 @@ use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; -use GraphQL\Validator\Messages; +use GraphQL\Validator\Rules\ProvidedNonNullArguments; class IntrospectionTest extends \PHPUnit_Framework_TestCase { @@ -20,137 +20,57 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase 'fields' => [] ])); - $request = <<<'EOD' - query IntrospectionTestQuery { - schemaType: __type(name: "__Schema") { - name - } - queryRootType: __type(name: "QueryRoot") { - name - } - __schema { - __typename - types { - __typename - kind - name - fields { - __typename - name - args { - __typename - name - type { ...TypeRef } - defaultValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - interfaces { - ...TypeRef - } - enumValues { - __typename - name - isDeprecated - deprecationReason - } - } - directives { - __typename - name - type { ...TypeRef } - onOperation - onFragment - onField - } - } - } - - fragment TypeRef on __Type { - __typename - kind - name - ofType { - __typename - kind - name - ofType { - __typename - kind - name - ofType { - __typename - kind - name - } - } - } - } -EOD; - - $expected = array( + $request = Introspection::getIntrospectionQuery(false); + $expected = array ( 'data' => - array( - 'schemaType' => - array( - 'name' => '__Schema', - ), - 'queryRootType' => - array( - 'name' => 'QueryRoot', - ), + array ( '__schema' => - array( - '__typename' => '__Schema', + array ( + 'mutationType' => NULL, + 'queryType' => + array ( + 'name' => 'QueryRoot', + ), 'types' => - array( + array ( 0 => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => 'QueryRoot', - 'fields' => - array( - ), + 'inputFields' => NULL, 'interfaces' => - array(), + array ( + ), 'enumValues' => NULL, + 'possibleTypes' => NULL, + 'fields' => [] ), 1 => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Schema', 'fields' => - array( + array ( 0 => - array( - '__typename' => '__Field', + array ( 'name' => 'types', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'LIST', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Type', ), @@ -161,19 +81,17 @@ EOD; 'deprecationReason' => NULL, ), 1 => - array( - '__typename' => '__Field', + array ( 'name' => 'queryType', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Type', 'ofType' => NULL, @@ -183,14 +101,13 @@ EOD; 'deprecationReason' => NULL, ), 2 => - array( - '__typename' => '__Field', + array ( 'name' => 'mutationType', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Type', 'ofType' => NULL, @@ -199,29 +116,25 @@ EOD; 'deprecationReason' => NULL, ), 3 => - array( - '__typename' => '__Field', + array ( 'name' => 'directives', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'LIST', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Directive', ), @@ -232,31 +145,31 @@ EOD; 'deprecationReason' => NULL, ), ), + 'inputFields' => NULL, 'interfaces' => - array(), + array ( + ), 'enumValues' => NULL, + 'possibleTypes' => NULL, ), 2 => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Type', 'fields' => - array( + array ( 0 => - array( - '__typename' => '__Field', + array ( 'name' => 'kind', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'ENUM', 'name' => '__TypeKind', 'ofType' => NULL, @@ -266,14 +179,13 @@ EOD; 'deprecationReason' => NULL, ), 1 => - array( - '__typename' => '__Field', + array ( 'name' => 'name', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'ofType' => NULL, @@ -282,14 +194,13 @@ EOD; 'deprecationReason' => NULL, ), 2 => - array( - '__typename' => '__Field', + array ( 'name' => 'description', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'ofType' => NULL, @@ -298,18 +209,15 @@ EOD; 'deprecationReason' => NULL, ), 3 => - array( - '__typename' => '__Field', + array ( 'name' => 'fields', 'args' => - array( + array ( 0 => - array( - '__typename' => '__InputValue', + array ( 'name' => 'includeDeprecated', 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'Boolean', 'ofType' => NULL, @@ -318,18 +226,15 @@ EOD; ), ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'LIST', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Field', 'ofType' => NULL, @@ -340,24 +245,21 @@ EOD; 'deprecationReason' => NULL, ), 4 => - array( - '__typename' => '__Field', + array ( 'name' => 'interfaces', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'LIST', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Type', 'ofType' => NULL, @@ -368,24 +270,21 @@ EOD; 'deprecationReason' => NULL, ), 5 => - array( - '__typename' => '__Field', + array ( 'name' => 'possibleTypes', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'LIST', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Type', 'ofType' => NULL, @@ -396,38 +295,32 @@ EOD; 'deprecationReason' => NULL, ), 6 => - array( - '__typename' => '__Field', + array ( 'name' => 'enumValues', 'args' => - array( + array ( 0 => - array( - '__typename' => '__InputValue', - 'defaultValue' => 'false', + array ( 'name' => 'includeDeprecated', 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'Boolean', 'ofType' => NULL, ), + 'defaultValue' => 'false', ), ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'LIST', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__EnumValue', 'ofType' => NULL, @@ -438,24 +331,21 @@ EOD; 'deprecationReason' => NULL, ), 7 => - array( - '__typename' => '__Field', + array ( 'name' => 'inputFields', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'LIST', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__InputValue', 'ofType' => NULL, @@ -466,14 +356,13 @@ EOD; 'deprecationReason' => NULL, ), 8 => - array( - '__typename' => '__Field', + array ( 'name' => 'ofType', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Type', 'ofType' => NULL, @@ -482,116 +371,111 @@ EOD; 'deprecationReason' => NULL, ), ), + 'inputFields' => NULL, 'interfaces' => - array(), + array ( + ), 'enumValues' => NULL, + 'possibleTypes' => NULL, ), 3 => - array( - '__typename' => '__Type', + array ( 'kind' => 'ENUM', 'name' => '__TypeKind', 'fields' => NULL, + 'inputFields' => NULL, 'interfaces' => NULL, 'enumValues' => - array( + array ( 0 => - array( - '__typename' => '__EnumValue', + array ( 'name' => 'SCALAR', 'isDeprecated' => false, 'deprecationReason' => NULL, ), 1 => - array( - '__typename' => '__EnumValue', + array ( 'name' => 'OBJECT', 'isDeprecated' => false, 'deprecationReason' => NULL, ), 2 => - array( - '__typename' => '__EnumValue', + array ( 'name' => 'INTERFACE', 'isDeprecated' => false, 'deprecationReason' => NULL, ), 3 => - array( - '__typename' => '__EnumValue', + array ( 'name' => 'UNION', 'isDeprecated' => false, 'deprecationReason' => NULL, ), 4 => - array( - '__typename' => '__EnumValue', + array ( 'name' => 'ENUM', 'isDeprecated' => false, 'deprecationReason' => NULL, ), 5 => - array( - '__typename' => '__EnumValue', + array ( 'name' => 'INPUT_OBJECT', 'isDeprecated' => false, 'deprecationReason' => NULL, ), 6 => - array( - '__typename' => '__EnumValue', + array ( 'name' => 'LIST', 'isDeprecated' => false, 'deprecationReason' => NULL, ), 7 => - array( - '__typename' => '__EnumValue', + array ( 'name' => 'NON_NULL', 'isDeprecated' => false, 'deprecationReason' => NULL, ), ), + 'possibleTypes' => NULL, ), 4 => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'fields' => NULL, + 'inputFields' => NULL, 'interfaces' => NULL, 'enumValues' => NULL, + 'possibleTypes' => NULL, ), 5 => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'Boolean', 'fields' => NULL, + 'inputFields' => NULL, 'interfaces' => NULL, 'enumValues' => NULL, + 'possibleTypes' => NULL, ), 6 => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Field', 'fields' => - array( + array ( 0 => - array( - '__typename' => '__Field', + array ( 'name' => 'name', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'ofType' => NULL, @@ -601,14 +485,13 @@ EOD; 'deprecationReason' => NULL, ), 1 => - array( - '__typename' => '__Field', + array ( 'name' => 'description', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'ofType' => NULL, @@ -617,29 +500,25 @@ EOD; 'deprecationReason' => NULL, ), 2 => - array( - '__typename' => '__Field', + array ( 'name' => 'args', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'LIST', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__InputValue', ), @@ -650,19 +529,17 @@ EOD; 'deprecationReason' => NULL, ), 3 => - array( - '__typename' => '__Field', + array ( 'name' => 'type', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Type', 'ofType' => NULL, @@ -672,19 +549,17 @@ EOD; 'deprecationReason' => NULL, ), 4 => - array( - '__typename' => '__Field', + array ( 'name' => 'isDeprecated', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'Boolean', 'ofType' => NULL, @@ -694,14 +569,13 @@ EOD; 'deprecationReason' => NULL, ), 5 => - array( - '__typename' => '__Field', + array ( 'name' => 'deprecationReason', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'ofType' => NULL, @@ -710,31 +584,31 @@ EOD; 'deprecationReason' => NULL, ), ), + 'inputFields' => NULL, 'interfaces' => - array(), + array ( + ), 'enumValues' => NULL, + 'possibleTypes' => NULL, ), 7 => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__InputValue', 'fields' => - array( + array ( 0 => - array( - '__typename' => '__Field', + array ( 'name' => 'name', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'ofType' => NULL, @@ -744,14 +618,13 @@ EOD; 'deprecationReason' => NULL, ), 1 => - array( - '__typename' => '__Field', + array ( 'name' => 'description', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'ofType' => NULL, @@ -760,19 +633,17 @@ EOD; 'deprecationReason' => NULL, ), 2 => - array( - '__typename' => '__Field', + array ( 'name' => 'type', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Type', 'ofType' => NULL, @@ -782,14 +653,13 @@ EOD; 'deprecationReason' => NULL, ), 3 => - array( - '__typename' => '__Field', + array ( 'name' => 'defaultValue', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'ofType' => NULL, @@ -798,31 +668,31 @@ EOD; 'deprecationReason' => NULL, ), ), + 'inputFields' => NULL, 'interfaces' => - array(), + array ( + ), 'enumValues' => NULL, + 'possibleTypes' => NULL, ), 8 => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__EnumValue', 'fields' => - array( + array ( 0 => - array( - '__typename' => '__Field', + array ( 'name' => 'name', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'ofType' => NULL, @@ -832,14 +702,13 @@ EOD; 'deprecationReason' => NULL, ), 1 => - array( - '__typename' => '__Field', + array ( 'name' => 'description', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'ofType' => NULL, @@ -848,19 +717,17 @@ EOD; 'deprecationReason' => NULL, ), 2 => - array( - '__typename' => '__Field', + array ( 'name' => 'isDeprecated', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'NON_NULL', 'name' => NULL, 'ofType' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'Boolean', 'ofType' => NULL, @@ -870,14 +737,13 @@ EOD; 'deprecationReason' => NULL, ), 3 => - array( - '__typename' => '__Field', + array ( 'name' => 'deprecationReason', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'ofType' => NULL, @@ -886,42 +752,47 @@ EOD; 'deprecationReason' => NULL, ), ), + 'inputFields' => NULL, 'interfaces' => - array(), + array ( + ), 'enumValues' => NULL, + 'possibleTypes' => NULL, ), 9 => - array( - '__typename' => '__Type', + array ( 'kind' => 'OBJECT', 'name' => '__Directive', 'fields' => - array( + array ( 0 => - array( - '__typename' => '__Field', + array ( 'name' => 'name', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', - 'kind' => 'SCALAR', - 'name' => 'String', - 'ofType' => NULL, + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + 'ofType' => NULL, + ), ), 'isDeprecated' => false, 'deprecationReason' => NULL, ), 1 => - array( - '__typename' => '__Field', + array ( 'name' => 'description', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', + array ( 'kind' => 'SCALAR', 'name' => 'String', 'ofType' => NULL, @@ -930,149 +801,192 @@ EOD; 'deprecationReason' => NULL, ), 2 => - array( - '__typename' => '__Field', - 'name' => 'type', + array ( + 'name' => 'args', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', - 'kind' => 'OBJECT', - 'name' => '__Type', - 'ofType' => NULL, + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__InputValue', + ), + ), + ), ), 'isDeprecated' => false, 'deprecationReason' => NULL, ), 3 => - array( - '__typename' => '__Field', + array ( 'name' => 'onOperation', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', - 'kind' => 'SCALAR', - 'name' => 'Boolean', - 'ofType' => NULL, + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => NULL, + ), ), 'isDeprecated' => false, 'deprecationReason' => NULL, ), 4 => - array( - '__typename' => '__Field', + array ( 'name' => 'onFragment', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', - 'kind' => 'SCALAR', - 'name' => 'Boolean', - 'ofType' => NULL, + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => NULL, + ), ), 'isDeprecated' => false, 'deprecationReason' => NULL, ), 5 => - array( - '__typename' => '__Field', + array ( 'name' => 'onField', 'args' => - array(), + array ( + ), 'type' => - array( - '__typename' => '__Type', - 'kind' => 'SCALAR', - 'name' => 'Boolean', - 'ofType' => NULL, + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => NULL, + ), ), 'isDeprecated' => false, 'deprecationReason' => NULL, ), ), + 'inputFields' => NULL, 'interfaces' => - array(), + array ( + ), 'enumValues' => NULL, + 'possibleTypes' => NULL, ), 10 => [ - '__typename' => '__Type', 'kind' => 'SCALAR', 'name' => 'ID', 'fields' => null, + 'inputFields' => null, 'interfaces' => null, 'enumValues' => null, + 'possibleTypes' => null ], 11 => [ - '__typename' => '__Type', 'kind' => 'SCALAR', 'name' => 'Float', 'fields' => null, + 'inputFields' => null, 'interfaces' => null, 'enumValues' => null, + 'possibleTypes' => null ], 12 => [ - '__typename' => '__Type', 'kind' => 'SCALAR', 'name' => 'Int', 'fields' => null, + 'inputFields' => null, 'interfaces' => null, 'enumValues' => null, - ] + 'possibleTypes' => null + ], ), 'directives' => - array( + array ( 0 => - array( - '__typename' => '__Directive', - 'name' => 'if', - 'type' => - array( - '__typename' => '__Type', - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array( - '__typename' => '__Type', - 'kind' => 'SCALAR', - 'name' => 'Boolean', - 'ofType' => NULL, + array ( + 'name' => 'include', + 'args' => + array ( + 0 => + array ( + 'defaultValue' => NULL, + 'name' => 'if', + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => NULL, + ), + ), ), ), 'onOperation' => false, - 'onFragment' => false, + 'onFragment' => true, 'onField' => true, ), 1 => - array( - '__typename' => '__Directive', - 'name' => 'unless', - 'type' => - array( - '__typename' => '__Type', - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array( - '__typename' => '__Type', - 'kind' => 'SCALAR', - 'name' => 'Boolean', - 'ofType' => NULL, + array ( + 'name' => 'skip', + 'args' => + array ( + 0 => + array ( + 'defaultValue' => NULL, + 'name' => 'if', + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => NULL, + ), + ), ), ), 'onOperation' => false, - 'onFragment' => false, + 'onFragment' => true, 'onField' => true, ), ), ), - ) + ), ); - $this->assertEquals($expected, GraphQL::execute($emptySchema, $request)); + $actual = GraphQL::execute($emptySchema, $request); + + $this->assertEquals($expected, $actual); } function testIntrospectsOnInputObject() @@ -1445,7 +1359,9 @@ EOD; '; $expected = [ 'errors' => [ - new FormattedError(Messages::missingArgMessage('__type', 'name', 'String!'), [new SourceLocation(3, 9)]) + FormattedError::create( + ProvidedNonNullArguments::missingFieldArgMessage('__type', 'name', 'String!'), [new SourceLocation(3, 9)] + ) ] ]; $this->assertEquals($expected, GraphQL::execute($schema, $request)); diff --git a/tests/Type/ScalarCoercionTest.php b/tests/Type/ScalarCoercionTest.php deleted file mode 100644 index f3257d1..0000000 --- a/tests/Type/ScalarCoercionTest.php +++ /dev/null @@ -1,63 +0,0 @@ -assertSame(1, $intType->coerce(1)); - $this->assertSame(0, $intType->coerce(0)); - $this->assertSame(0, $intType->coerce(0.1)); - $this->assertSame(1, $intType->coerce(1.1)); - $this->assertSame(-1, $intType->coerce(-1.1)); - $this->assertSame(100000, $intType->coerce(1e5)); - $this->assertSame(null, $intType->coerce(1e100)); - $this->assertSame(null, $intType->coerce(-1e100)); - $this->assertSame(-1, $intType->coerce('-1.1')); - $this->assertSame(null, $intType->coerce('one')); - $this->assertSame(0, $intType->coerce(false)); - } - - public function testCoercesOutputFloat() - { - $floatType = Type::float(); - - $this->assertSame(1.0, $floatType->coerce(1)); - $this->assertSame(-1.0, $floatType->coerce(-1)); - $this->assertSame(0.1, $floatType->coerce(0.1)); - $this->assertSame(1.1, $floatType->coerce(1.1)); - $this->assertSame(-1.1, $floatType->coerce(-1.1)); - $this->assertSame(null, $floatType->coerce('one')); - $this->assertSame(0.0, $floatType->coerce(false)); - $this->assertSame(1.0, $floatType->coerce(true)); - } - - public function testCoercesOutputStrings() - { - $stringType = Type::string(); - - $this->assertSame('string', $stringType->coerce('string')); - $this->assertSame('1', $stringType->coerce(1)); - $this->assertSame('-1.1', $stringType->coerce(-1.1)); - $this->assertSame('true', $stringType->coerce(true)); - $this->assertSame('false', $stringType->coerce(false)); - } - - public function testCoercesOutputBoolean() - { - $boolType = Type::boolean(); - - $this->assertSame(true, $boolType->coerce('string')); - $this->assertSame(false, $boolType->coerce('')); - $this->assertSame(true, $boolType->coerce(1)); - $this->assertSame(false, $boolType->coerce(0)); - $this->assertSame(true, $boolType->coerce(true)); - $this->assertSame(false, $boolType->coerce(false)); - - // TODO: how should it behaive on '0'? - } -} \ No newline at end of file diff --git a/tests/Type/ScalarSerializationTest.php b/tests/Type/ScalarSerializationTest.php new file mode 100644 index 0000000..80ef84b --- /dev/null +++ b/tests/Type/ScalarSerializationTest.php @@ -0,0 +1,63 @@ +assertSame(1, $intType->serialize(1)); + $this->assertSame(0, $intType->serialize(0)); + $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)); + $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)); + } + + public function testCoercesOutputFloat() + { + $floatType = Type::float(); + + $this->assertSame(1.0, $floatType->serialize(1)); + $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(null, $floatType->serialize('one')); + $this->assertSame(0.0, $floatType->serialize(false)); + $this->assertSame(1.0, $floatType->serialize(true)); + } + + public function testCoercesOutputStrings() + { + $stringType = Type::string(); + + $this->assertSame('string', $stringType->serialize('string')); + $this->assertSame('1', $stringType->serialize(1)); + $this->assertSame('-1.1', $stringType->serialize(-1.1)); + $this->assertSame('true', $stringType->serialize(true)); + $this->assertSame('false', $stringType->serialize(false)); + } + + public function testCoercesOutputBoolean() + { + $boolType = Type::boolean(); + + $this->assertSame(true, $boolType->serialize('string')); + $this->assertSame(false, $boolType->serialize('')); + $this->assertSame(true, $boolType->serialize(1)); + $this->assertSame(false, $boolType->serialize(0)); + $this->assertSame(true, $boolType->serialize(true)); + $this->assertSame(false, $boolType->serialize(false)); + + // TODO: how should it behaive on '0'? + } +} \ No newline at end of file diff --git a/tests/Type/SchemaValidatorTest.php b/tests/Type/SchemaValidatorTest.php index cb08cc9..402f964 100644 --- a/tests/Type/SchemaValidatorTest.php +++ b/tests/Type/SchemaValidatorTest.php @@ -29,10 +29,8 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase public function testPassesOnTheIntrospectionSchema() { $schema = new Schema(Introspection::_schema()); - $validationResult = SchemaValidator::validate($schema); - - $this->assertSame(true, $validationResult->isValid); - $this->assertSame(null, $validationResult->errors); + $errors = SchemaValidator::validate($schema); + $this->assertEmpty($errors); } @@ -65,12 +63,11 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase $schema = new Schema($someOutputType); $validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]); - $this->assertSame(false, $validationResult->isValid); - $this->assertSame(1, count($validationResult->errors)); + $this->assertSame(1, count($validationResult)); $this->assertSame( 'Field SomeOutputType.sneaky is of type SomeInputType, which is an ' . 'input type, but field types must be output types!', - $validationResult->errors[0]->message + $validationResult[0]->message ); } } @@ -93,17 +90,17 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase ]); $schema = new Schema($someOutputType); - $validationResult = SchemaValidator::validate($schema, [$rule]); - $this->assertSame(true, $validationResult->isValid); + $errors = SchemaValidator::validate($schema, [$rule]); + $this->assertEmpty($errors); } - private function checkValidationResult($validationResult, $operationType) + private function checkValidationResult($validationErrors, $operationType) { - $this->assertEquals(false, $validationResult->isValid); - $this->assertEquals(1, count($validationResult->errors)); + $this->assertNotEmpty($validationErrors, "Should not validate"); + $this->assertEquals(1, count($validationErrors)); $this->assertEquals( "Schema $operationType type SomeInputType must be an object type!", - $validationResult->errors[0]->message + $validationErrors[0]->message ); } @@ -197,8 +194,8 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase private function assertAcceptingFieldArgOfType($fieldArgType) { $schema = $this->schemaWithFieldArgOfType($fieldArgType); - $validationResult = SchemaValidator::validate($schema, [SchemaValidator::noOutputTypesAsInputArgsRule()]); - $this->assertSame(true, $validationResult->isValid); + $errors = SchemaValidator::validate($schema, [SchemaValidator::noOutputTypesAsInputArgsRule()]); + $this->assertEmpty($errors); } private function schemaWithFieldArgOfType($argType) @@ -223,14 +220,13 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase return new Schema($queryType); } - private function expectRejectionBecauseFieldIsNotInputType($validationResult, $fieldTypeName) + private function expectRejectionBecauseFieldIsNotInputType($errors, $fieldTypeName) { - $this->assertSame(false, $validationResult->isValid); - $this->assertSame(1, count($validationResult->errors)); + $this->assertSame(1, count($errors)); $this->assertSame( "Input field SomeIncorrectInputType.val has type $fieldTypeName, " . "which is not an input type!", - $validationResult->errors[0]->message + $errors[0]->message ); } @@ -245,40 +241,9 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase public function testRejectsWhenAPossibleTypeDoesNotImplementTheInterface() { - // rejects when a possible type does not implement the interface - $InterfaceType = new InterfaceType([ - 'name' => 'InterfaceType', - 'fields' => [] - ]); - - $SubType = new ObjectType([ - 'name' => 'SubType', - 'fields' => [], - 'interfaces' => [] - ]); - - InterfaceType::addImplementationToInterfaces($SubType, [$InterfaceType]); - - // Sanity check. - $this->assertEquals(1, count($InterfaceType->getPossibleTypes())); - $this->assertEquals($SubType, $InterfaceType->getPossibleTypes()[0]); - - - $schema = new Schema($InterfaceType); - $validationResult = SchemaValidator::validate( - $schema, - [SchemaValidator::interfacePossibleTypesMustImplementTheInterfaceRule()] - ); - $this->assertSame(false, $validationResult->isValid); - $this->assertSame(1, count($validationResult->errors)); - $this->assertSame( - 'SubType is a possible type of interface InterfaceType but does not ' . - 'implement it!', - $validationResult->errors[0]->message - ); + // TODO: Validation for interfaces / implementors } - private function assertAcceptingAnInterfaceWithANormalSubtype($rule) { $interfaceType = new InterfaceType([ @@ -294,8 +259,8 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase $schema = new Schema($interfaceType, $subType); - $validationResult = SchemaValidator::validate($schema, [$rule]); - $this->assertSame(true, $validationResult->isValid); + $errors = SchemaValidator::validate($schema, [$rule]); + $this->assertEmpty($errors); } @@ -337,28 +302,12 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase // Another sanity check. $this->assertSame($subType, $schema->getType('SubType')); - $validationResult = SchemaValidator::validate($schema, [SchemaValidator::typesInterfacesMustShowThemAsPossibleRule()]); - $this->assertSame(false, $validationResult->isValid); - $this->assertSame(1, count($validationResult->errors)); + $errors = SchemaValidator::validate($schema, [SchemaValidator::typesInterfacesMustShowThemAsPossibleRule()]); + $this->assertSame(1, count($errors)); $this->assertSame( 'SubType implements interface InterfaceType, but InterfaceType does ' . 'not list it as possible!', - $validationResult->errors[0]->message + $errors[0]->message ); - - -/* - - var validationResult = validateSchema( - schema, - [TypesInterfacesMustShowThemAsPossible] - ); - expect(validationResult.isValid).to.equal(false); - expect(validationResult.errors.length).to.equal(1); - expect(validationResult.errors[0].message).to.equal( - 'SubType implements interface InterfaceType, but InterfaceType does ' + - 'not list it as possible!' - ); - */ } } diff --git a/tests/Validator/ArgumentsOfCorrectTypeTest.php b/tests/Validator/ArgumentsOfCorrectTypeTest.php index e8c962f..74e5181 100644 --- a/tests/Validator/ArgumentsOfCorrectTypeTest.php +++ b/tests/Validator/ArgumentsOfCorrectTypeTest.php @@ -9,7 +9,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase { function missingArg($fieldName, $argName, $typeName, $line, $column) { - return new FormattedError( + return FormattedError::create( Messages::missingArgMessage($fieldName, $argName, $typeName), [new SourceLocation($line, $column)] ); @@ -17,8 +17,8 @@ class ArgumentsOfCorrectTypeTest extends TestCase function badValue($argName, $typeName, $value, $line, $column) { - return new FormattedError( - Messages::badValueMessage($argName, $typeName, $value), + return FormattedError::create( + ArgumentsOfCorrectType::badValueMessage($argName, $typeName, $value), [new SourceLocation($line, $column)] ); } @@ -632,33 +632,6 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } - public function testMissingOneNonNullableArgument() - { - $this->expectFailsRule(new ArgumentsOfCorrectType, ' - { - complicatedArgs { - multipleReqs(req2: 2) - } - } - ', [ - $this->missingArg('multipleReqs', 'req1', 'Int!', 4, 13) - ]); - } - - public function testMissingMultipleNonNullableArguments() - { - $this->expectFailsRule(new ArgumentsOfCorrectType, ' - { - complicatedArgs { - multipleReqs - } - } - ', [ - $this->missingArg('multipleReqs', 'req1', 'Int!', 4, 13), - $this->missingArg('multipleReqs', 'req2', 'Int!', 4, 13), - ]); - } - public function testIncorrectValueAndMissingArgument() { $this->expectFailsRule(new ArgumentsOfCorrectType, ' @@ -668,7 +641,6 @@ class ArgumentsOfCorrectTypeTest extends TestCase } } ', [ - $this->missingArg('multipleReqs', 'req2', 'Int!', 4, 13), $this->badValue('req1', 'Int!', '"one"', 4, 32), ]); } diff --git a/tests/Validator/DefaultValuesOfCorrectTypeTest.php b/tests/Validator/DefaultValuesOfCorrectTypeTest.php index 3bfddec..06a22d6 100644 --- a/tests/Validator/DefaultValuesOfCorrectTypeTest.php +++ b/tests/Validator/DefaultValuesOfCorrectTypeTest.php @@ -93,7 +93,7 @@ class DefaultValuesOfCorrectTypeTest extends TestCase private function defaultForNonNullArg($varName, $typeName, $guessTypeName, $line, $column) { - return new FormattedError( + return FormattedError::create( Messages::defaultForNonNullArgMessage($varName, $typeName, $guessTypeName), [ new SourceLocation($line, $column) ] ); @@ -101,7 +101,7 @@ class DefaultValuesOfCorrectTypeTest extends TestCase private function badValue($varName, $typeName, $val, $line, $column) { - return new FormattedError( + return FormattedError::create( Messages::badValueForDefaultArgMessage($varName, $typeName, $val), [ new SourceLocation($line, $column) ] ); diff --git a/tests/Validator/FieldsOnCorrectTypeTest.php b/tests/Validator/FieldsOnCorrectTypeTest.php index b723750..bb1c61e 100644 --- a/tests/Validator/FieldsOnCorrectTypeTest.php +++ b/tests/Validator/FieldsOnCorrectTypeTest.php @@ -56,6 +56,15 @@ class FieldsOnCorrectTypeTest extends TestCase '); } + public function testIgnoresFieldsOnUnknownType() + { + $this->expectPassesRule(new FieldsOnCorrectType, ' + fragment unknownSelection on UnknownType { + unknownField + } + '); + } + public function testFieldNotDefinedOnFragment() { $this->expectFailsRule(new FieldsOnCorrectType, ' @@ -184,7 +193,7 @@ class FieldsOnCorrectTypeTest extends TestCase private function undefinedField($field, $type, $line, $column) { - return new FormattedError( + return FormattedError::create( Messages::undefinedFieldMessage($field, $type), [new SourceLocation($line, $column)] ); diff --git a/tests/Validator/FragmentsOnCompositeTypesTest.php b/tests/Validator/FragmentsOnCompositeTypesTest.php index aaa15d3..1f1a261 100644 --- a/tests/Validator/FragmentsOnCompositeTypesTest.php +++ b/tests/Validator/FragmentsOnCompositeTypesTest.php @@ -86,8 +86,8 @@ class FragmentsOnCompositeTypesTest extends TestCase } } ', - [new FormattedError( - Messages::inlineFragmentOnNonCompositeErrorMessage('String'), + [FormattedError::create( + FragmentsOnCompositeTypes::inlineFragmentOnNonCompositeErrorMessage('String'), [new SourceLocation(3, 16)] )] ); @@ -95,8 +95,8 @@ class FragmentsOnCompositeTypesTest extends TestCase private function error($fragName, $typeName, $line, $column) { - return new FormattedError( - Messages::fragmentOnNonCompositeErrorMessage($fragName, $typeName), + return FormattedError::create( + FragmentsOnCompositeTypes::fragmentOnNonCompositeErrorMessage($fragName, $typeName), [ new SourceLocation($line, $column) ] ); } diff --git a/tests/Validator/KnownArgumentNamesTest.php b/tests/Validator/KnownArgumentNamesTest.php index 7357169..fafd944 100644 --- a/tests/Validator/KnownArgumentNamesTest.php +++ b/tests/Validator/KnownArgumentNamesTest.php @@ -26,6 +26,15 @@ class KnownArgumentNamesTest extends TestCase '); } + public function testIgnoresArgsOfUnknownFields() + { + $this->expectPassesRule(new KnownArgumentNames, ' + fragment argOnUnknownField on Dog { + unknownField(unknownArg: SIT) + } + '); + } + public function testMultipleArgsInReverseOrderAreKnown() { $this->expectPassesRule(new KnownArgumentNames, ' @@ -62,6 +71,26 @@ class KnownArgumentNamesTest extends TestCase '); } + public function testDirectiveArgsAreKnown() + { + $this->expectPassesRule(new KnownArgumentNames, ' + { + dog @skip(if: true) + } + '); + } + + public function testUndirectiveArgsAreInvalid() + { + $this->expectFailsRule(new KnownArgumentNames, ' + { + dog @skip(unless: true) + } + ', [ + $this->unknownDirectiveArg('unless', 'skip', 3, 19), + ]); + } + public function testInvalidArgName() { $this->expectFailsRule(new KnownArgumentNames, ' @@ -106,28 +135,18 @@ class KnownArgumentNamesTest extends TestCase ]); } - public function testArgsMayBeOnObjectButNotInterface() - { - $this->expectFailsRule(new KnownArgumentNames, ' - fragment nameSometimesHasArg on Being { - name(surname: true) - ... on Human { - name(surname: true) - } - ... on Dog { - name(surname: true) - } - } - ', [ - $this->unknownArg('surname', 'name', 'Being', 3, 14), - $this->unknownArg('surname', 'name', 'Dog', 8, 16) - ]); - } - private function unknownArg($argName, $fieldName, $typeName, $line, $column) { - return new FormattedError( - Messages::unknownArgMessage($argName, $fieldName, $typeName), + return FormattedError::create( + KnownArgumentNames::unknownArgMessage($argName, $fieldName, $typeName), + [new SourceLocation($line, $column)] + ); + } + + private function unknownDirectiveArg($argName, $directiveName, $line, $column) + { + return FormattedError::create( + KnownArgumentNames::unknownDirectiveArgMessage($argName, $directiveName), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/KnownDirectivesTest.php b/tests/Validator/KnownDirectivesTest.php index 8983db7..6f2d4a3 100644 --- a/tests/Validator/KnownDirectivesTest.php +++ b/tests/Validator/KnownDirectivesTest.php @@ -26,10 +26,10 @@ class KnownDirectivesTest extends TestCase { $this->expectPassesRule(new KnownDirectives, ' { - dog @if: true { + dog @include(if: true) { name } - human @unless: false { + human @skip(if: true) { name } } @@ -40,7 +40,7 @@ class KnownDirectivesTest extends TestCase { $this->expectFailsRule(new KnownDirectives, ' { - dog @unknown: "directive" { + dog @unknown(directive: "value") { name } } @@ -53,12 +53,12 @@ class KnownDirectivesTest extends TestCase { $this->expectFailsRule(new KnownDirectives, ' { - dog @unknown: "directive" { + dog @unknown(directive: "value") { name } - human @unknown: "directive" { + human @unknown(directive: "value") { name - pets @unknown: "directive" { + pets @unknown(directive: "value") { name } } @@ -70,30 +70,42 @@ class KnownDirectivesTest extends TestCase ]); } + public function testWithWellPlacedDirectives() + { + $this->expectPassesRule(new KnownDirectives, ' + query Foo { + name @include(if: true) + ...Frag @include(if: true) + skippedField @skip(if: true) + ...SkippedFrag @skip(if: true) + } + '); + } + public function testWithMisplacedDirectives() { $this->expectFailsRule(new KnownDirectives, ' - query Foo @if: true { + query Foo @include(if: true) { name ...Frag } ', [ - $this->misplacedDirective('if', 'operation', 2, 17) + $this->misplacedDirective('include', 'operation', 2, 17) ]); } private function unknownDirective($directiveName, $line, $column) { - return new FormattedError( - Messages::unknownDirectiveMessage($directiveName), + return FormattedError::create( + KnownDirectives::unknownDirectiveMessage($directiveName), [ new SourceLocation($line, $column) ] ); } function misplacedDirective($directiveName, $placement, $line, $column) { - return new FormattedError( - Messages::misplacedDirectiveMessage($directiveName, $placement), + return FormattedError::create( + KnownDirectives::misplacedDirectiveMessage($directiveName, $placement), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/KnownFragmentNamesTest.php b/tests/Validator/KnownFragmentNamesTest.php index 2ff97c0..f79857e 100644 --- a/tests/Validator/KnownFragmentNamesTest.php +++ b/tests/Validator/KnownFragmentNamesTest.php @@ -57,8 +57,8 @@ class KnownFragmentNamesTest extends TestCase private function undefFrag($fragName, $line, $column) { - return new FormattedError( - "Undefined fragment $fragName.", + return FormattedError::create( + KnownFragmentNames::unknownFragmentMessage($fragName), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/KnownTypeNamesTest.php b/tests/Validator/KnownTypeNamesTest.php index 2ffd19f..635616e 100644 --- a/tests/Validator/KnownTypeNamesTest.php +++ b/tests/Validator/KnownTypeNamesTest.php @@ -44,8 +44,8 @@ class KnownTypeNamesTest extends TestCase private function unknownType($typeName, $line, $column) { - return new FormattedError( - Messages::unknownTypeMessage($typeName), + return FormattedError::create( + KnownTypeNames::unknownTypeMessage($typeName), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/NoFragmentCyclesTest.php b/tests/Validator/NoFragmentCyclesTest.php index 4b5daf8..8dd1d3a 100644 --- a/tests/Validator/NoFragmentCyclesTest.php +++ b/tests/Validator/NoFragmentCyclesTest.php @@ -2,9 +2,7 @@ namespace GraphQL\Validator; use GraphQL\FormattedError; -use GraphQL\Language\Source; use GraphQL\Language\SourceLocation; -use GraphQL\Type\Definition\Config; use GraphQL\Validator\Rules\NoFragmentCycles; class NoFragmentCyclesTest extends TestCase @@ -88,8 +86,8 @@ class NoFragmentCyclesTest extends TestCase fragment fragA on Dog { ...fragB } fragment fragB on Dog { ...fragA } ', [ - new FormattedError( - Messages::cycleErrorMessage('fragA', ['fragB']), + FormattedError::create( + NoFragmentCycles::cycleErrorMessage('fragA', ['fragB']), [ new SourceLocation(2, 31), new SourceLocation(3, 31) ] ) ]); @@ -101,8 +99,8 @@ class NoFragmentCyclesTest extends TestCase fragment fragB on Dog { ...fragA } fragment fragA on Dog { ...fragB } ', [ - new FormattedError( - Messages::cycleErrorMessage('fragB', ['fragA']), + FormattedError::create( + NoFragmentCycles::cycleErrorMessage('fragB', ['fragA']), [new SourceLocation(2, 31), new SourceLocation(3, 31)] ) ]); @@ -122,8 +120,8 @@ class NoFragmentCyclesTest extends TestCase } } ', [ - new FormattedError( - Messages::cycleErrorMessage('fragA', ['fragB']), + FormattedError::create( + NoFragmentCycles::cycleErrorMessage('fragA', ['fragB']), [new SourceLocation(4, 11), new SourceLocation(9, 11)] ) ]); @@ -140,8 +138,8 @@ class NoFragmentCyclesTest extends TestCase fragment fragZ on Dog { ...fragO } fragment fragO on Dog { ...fragA, ...fragX } ', [ - new FormattedError( - Messages::cycleErrorMessage('fragA', ['fragB', 'fragC', 'fragO']), + FormattedError::create( + NoFragmentCycles::cycleErrorMessage('fragA', ['fragB', 'fragC', 'fragO']), [ new SourceLocation(2, 31), new SourceLocation(3, 31), @@ -149,8 +147,8 @@ class NoFragmentCyclesTest extends TestCase new SourceLocation(8, 31), ] ), - new FormattedError( - Messages::cycleErrorMessage('fragX', ['fragY', 'fragZ', 'fragO']), + FormattedError::create( + NoFragmentCycles::cycleErrorMessage('fragX', ['fragY', 'fragZ', 'fragO']), [ new SourceLocation(5, 31), new SourceLocation(6, 31), @@ -168,12 +166,12 @@ class NoFragmentCyclesTest extends TestCase fragment fragB on Dog { ...fragA } fragment fragC on Dog { ...fragA } ', [ - new FormattedError( - 'Cannot spread fragment fragA within itself via fragB.', + FormattedError::create( + NoFragmentCycles::cycleErrorMessage('fragA', ['fragB']), [new SourceLocation(2, 31), new SourceLocation(3, 31)] ), - new FormattedError( - 'Cannot spread fragment fragA within itself via fragC.', + FormattedError::create( + NoFragmentCycles::cycleErrorMessage('fragA', ['fragC']), [new SourceLocation(2, 41), new SourceLocation(4, 31)] ) ]); @@ -181,8 +179,8 @@ class NoFragmentCyclesTest extends TestCase private function cycleError($fargment, $spreadNames, $line, $column) { - return new FormattedError( - Messages::cycleErrorMessage($fargment, $spreadNames), + return FormattedError::create( + NoFragmentCycles::cycleErrorMessage($fargment, $spreadNames), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/NoUndefinedVariablesTest.php b/tests/Validator/NoUndefinedVariablesTest.php index acf0fe9..ee4b249 100644 --- a/tests/Validator/NoUndefinedVariablesTest.php +++ b/tests/Validator/NoUndefinedVariablesTest.php @@ -302,16 +302,16 @@ class NoUndefinedVariablesTest extends TestCase private function undefVar($varName, $line, $column) { - return new FormattedError( - Messages::undefinedVarMessage($varName), + return FormattedError::create( + NoUndefinedVariables::undefinedVarMessage($varName), [new SourceLocation($line, $column)] ); } private function undefVarByOp($varName, $l1, $c1, $opName, $l2, $c2) { - return new FormattedError( - Messages::undefinedVarByOpMessage($varName, $opName), + return FormattedError::create( + NoUndefinedVariables::undefinedVarByOpMessage($varName, $opName), [new SourceLocation($l1, $c1), new SourceLocation($l2, $c2)] ); } diff --git a/tests/Validator/NoUnusedFragmentsTest.php b/tests/Validator/NoUnusedFragmentsTest.php index b201421..7ba79a5 100644 --- a/tests/Validator/NoUnusedFragmentsTest.php +++ b/tests/Validator/NoUnusedFragmentsTest.php @@ -130,10 +130,27 @@ class NoUnusedFragmentsTest extends TestCase ]); } + public function testContainsUnknownAndUndefFragments() + { + + $this->expectFailsRule(new NoUnusedFragments, ' + query Foo { + human(id: 4) { + ...bar + } + } + fragment foo on Human { + name + } + ', [ + $this->unusedFrag('foo', 7, 7), + ]); + } + private function unusedFrag($fragName, $line, $column) { - return new FormattedError( - Messages::unusedFragMessage($fragName), + return FormattedError::create( + NoUnusedFragments::unusedFragMessage($fragName), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/NoUnusedVariablesTest.php b/tests/Validator/NoUnusedVariablesTest.php index 1e8398a..00218ec 100644 --- a/tests/Validator/NoUnusedVariablesTest.php +++ b/tests/Validator/NoUnusedVariablesTest.php @@ -213,8 +213,8 @@ class NoUnusedVariablesTest extends TestCase private function unusedVar($varName, $line, $column) { - return new FormattedError( - Messages::unusedVariableMessage($varName), + return FormattedError::create( + NoUnusedVariables::unusedVariableMessage($varName), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/OverlappingFieldsCanBeMergedTest.php b/tests/Validator/OverlappingFieldsCanBeMergedTest.php index 7d32219..d326a79 100644 --- a/tests/Validator/OverlappingFieldsCanBeMergedTest.php +++ b/tests/Validator/OverlappingFieldsCanBeMergedTest.php @@ -48,8 +48,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase { $this->expectPassesRule(new OverlappingFieldsCanBeMerged, ' fragment mergeSameFieldsWithSameDirectives on Dog { - name @if:true - name @if:true + name @include(if: true) + name @include(if: true) } '); } @@ -68,8 +68,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase { $this->expectPassesRule(new OverlappingFieldsCanBeMerged, ' fragment differentDirectivesWithDifferentAliases on Dog { - nameIfTrue : name @if:true - nameIfFalse : name @if:false + nameIfTrue : name @include(if: true) + nameIfFalse : name @include(if: false) } '); } @@ -82,8 +82,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase fido : nickname } ', [ - new FormattedError( - Messages::fieldsConflictMessage('fido', 'name and nickname are different fields'), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('fido', 'name and nickname are different fields'), [new SourceLocation(3, 9), new SourceLocation(4, 9)] ) ]); @@ -97,8 +97,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase name } ', [ - new FormattedError( - Messages::fieldsConflictMessage('name', 'nickname and name are different fields'), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('name', 'nickname and name are different fields'), [new SourceLocation(3, 9), new SourceLocation(4, 9)] ) ]); @@ -112,8 +112,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase doesKnowCommand(dogCommand: HEEL) } ', [ - new FormattedError( - Messages::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'), [new SourceLocation(3,9), new SourceLocation(4,9)] ) ]); @@ -123,27 +123,43 @@ class OverlappingFieldsCanBeMergedTest extends TestCase { $this->expectFailsRule(new OverlappingFieldsCanBeMerged, ' fragment conflictingDirectiveArgs on Dog { - name @if: true - name @unless: false + name @include(if: true) + name @skip(if: true) } ', [ - new FormattedError( - Messages::fieldsConflictMessage('name', 'they have differing directives'), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('name', 'they have differing directives'), [new SourceLocation(3, 9), new SourceLocation(4, 9)] ) ]); } + public function testConflictingDirectiveArgs() + { + $this->expectFailsRule(new OverlappingFieldsCanBeMerged, ' + fragment conflictingDirectiveArgs on Dog { + name @include(if: true) + name @include(if: false) + } + ', [ + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('name', 'they have differing directives'), + [new SourceLocation(3, 9), new SourceLocation(4, 9)] + ) + ] + ); + } + public function testConflictingArgsWithMatchingDirectives() { $this->expectFailsRule(new OverlappingFieldsCanBeMerged, ' fragment conflictingArgsWithMatchingDirectiveArgs on Dog { - doesKnowCommand(dogCommand: SIT) @if:true - doesKnowCommand(dogCommand: HEEL) @if:true + doesKnowCommand(dogCommand: SIT) @include(if: true) + doesKnowCommand(dogCommand: HEEL) @include(if: true) } ', [ - new FormattedError( - Messages::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'), [new SourceLocation(3, 9), new SourceLocation(4, 9)] ) ]); @@ -153,12 +169,12 @@ class OverlappingFieldsCanBeMergedTest extends TestCase { $this->expectFailsRule(new OverlappingFieldsCanBeMerged, ' fragment conflictingDirectiveArgsWithMatchingArgs on Dog { - doesKnowCommand(dogCommand: SIT) @if: true - doesKnowCommand(dogCommand: SIT) @unless: false + doesKnowCommand(dogCommand: SIT) @include(if: true) + doesKnowCommand(dogCommand: SIT) @skip(if: true) } ', [ - new FormattedError( - Messages::fieldsConflictMessage('doesKnowCommand', 'they have differing directives'), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing directives'), [new SourceLocation(3, 9), new SourceLocation(4, 9)] ) ]); @@ -178,8 +194,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase x: b } ', [ - new FormattedError( - Messages::fieldsConflictMessage('x', 'a and b are different fields'), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'), [new SourceLocation(7, 9), new SourceLocation(10, 9)] ) ]); @@ -210,16 +226,16 @@ class OverlappingFieldsCanBeMergedTest extends TestCase x: b } ', [ - new FormattedError( - Messages::fieldsConflictMessage('x', 'a and b are different fields'), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'), [new SourceLocation(18, 9), new SourceLocation(21, 9)] ), - new FormattedError( - Messages::fieldsConflictMessage('x', 'a and c are different fields'), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and c are different fields'), [new SourceLocation(18, 9), new SourceLocation(14, 11)] ), - new FormattedError( - Messages::fieldsConflictMessage('x', 'b and c are different fields'), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'b and c are different fields'), [new SourceLocation(21, 9), new SourceLocation(14, 11)] ) ]); @@ -237,8 +253,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase } } ', [ - new FormattedError( - Messages::fieldsConflictMessage('field', [['x', 'a and b are different fields']]), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [['x', 'a and b are different fields']]), [ new SourceLocation(3, 9), new SourceLocation(6,9), @@ -263,8 +279,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase } } ', [ - new FormattedError( - Messages::fieldsConflictMessage('field', [ + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [ ['x', 'a and b are different fields'], ['y', 'c and d are different fields'] ]), @@ -296,8 +312,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase } } ', [ - new FormattedError( - Messages::fieldsConflictMessage('field', [['deepField', [['x', 'a and b are different fields']]]]), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [['deepField', [['x', 'a and b are different fields']]]]), [ new SourceLocation(3,9), new SourceLocation(8,9), @@ -329,8 +345,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase } } ', [ - new FormattedError( - Messages::fieldsConflictMessage('deepField', [['x', 'a and b are different fields']]), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('deepField', [['x', 'a and b are different fields']]), [ new SourceLocation(4,11), new SourceLocation(7,11), @@ -356,8 +372,8 @@ class OverlappingFieldsCanBeMergedTest extends TestCase } } ', [ - new FormattedError( - Messages::fieldsConflictMessage('scalar', 'they return differing types Int and String'), + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('scalar', 'they return differing types Int and String'), [ new SourceLocation(5,15), new SourceLocation(8,15) ] ) ]); @@ -379,6 +395,55 @@ class OverlappingFieldsCanBeMergedTest extends TestCase '); } + public function testComparesDeepTypesIncludingList() + { + $this->expectFailsRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, ' + { + connection { + ...edgeID + edges { + node { + id: name + } + } + } + } + + fragment edgeID on Connection { + edges { + node { + id + } + } + } + ', [ + FormattedError::create( + OverlappingFieldsCanBeMerged::fieldsConflictMessage('edges', [['node', [['id', 'id and name are different fields']]]]), + [ + new SourceLocation(14, 11), new SourceLocation(5, 13), + new SourceLocation(15, 13), new SourceLocation(6, 15), + new SourceLocation(16, 15), new SourceLocation(7, 17), + ] + ) + ]); + } + + public function testIgnoresUnknownTypes() + { + $this->expectPassesRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, ' + { + boxUnion { + ...on UnknownType { + scalar + } + ...on NonNullStringBox2 { + scalar + } + } + } + '); + } + private function getTestSchema() { $StringBox = new ObjectType([ @@ -411,13 +476,37 @@ class OverlappingFieldsCanBeMergedTest extends TestCase $BoxUnion = new UnionType([ 'name' => 'BoxUnion', + 'resolveType' => function() use ($StringBox) {return $StringBox;}, 'types' => [ $StringBox, $IntBox, $NonNullStringBox1, $NonNullStringBox2 ] ]); + $Connection = new ObjectType([ + 'name' => 'Connection', + 'fields' => [ + 'edges' => [ + 'type' => Type::listOf(new ObjectType([ + 'name' => 'Edge', + 'fields' => [ + 'node' => [ + 'type' => new ObjectType([ + 'name' => 'Node', + 'fields' => [ + 'id' => ['type' => Type::id()], + 'name' => ['type' => Type::string()] + ] + ]) + ] + ] + ])) + ] + ] + ]); + $schema = new Schema(new ObjectType([ 'name' => 'QueryRoot', 'fields' => [ - 'boxUnion' => ['type' => $BoxUnion ] + 'boxUnion' => ['type' => $BoxUnion ], + 'connection' => ['type' => $Connection] ] ])); diff --git a/tests/Validator/PossibleFragmentSpreadsTest.php b/tests/Validator/PossibleFragmentSpreadsTest.php index 6176e78..3e3b9c1 100644 --- a/tests/Validator/PossibleFragmentSpreadsTest.php +++ b/tests/Validator/PossibleFragmentSpreadsTest.php @@ -210,16 +210,16 @@ class PossibleFragmentSpreadsTest extends TestCase private function error($fragName, $parentType, $fragType, $line, $column) { - return new FormattedError( - Messages::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), + return FormattedError::create( + PossibleFragmentSpreads::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), [new SourceLocation($line, $column)] ); } private function errorAnon($parentType, $fragType, $line, $column) { - return new FormattedError( - Messages::typeIncompatibleAnonSpreadMessage($parentType, $fragType), + return FormattedError::create( + PossibleFragmentSpreads::typeIncompatibleAnonSpreadMessage($parentType, $fragType), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/ProvidedNonNullArgumentsTest.php b/tests/Validator/ProvidedNonNullArgumentsTest.php new file mode 100644 index 0000000..cba4a06 --- /dev/null +++ b/tests/Validator/ProvidedNonNullArgumentsTest.php @@ -0,0 +1,217 @@ +expectPassesRule(new ProvidedNonNullArguments, ' + { + dog { + isHousetrained(unknownArgument: true) + } + } + '); + } + + // Valid non-nullable value: + public function testArgOnOptionalArg() + { + $this->expectPassesRule(new ProvidedNonNullArguments, ' + { + dog { + isHousetrained(atOtherHomes: true) + } + } + '); + } + + public function testMultipleArgs() + { + $this->expectPassesRule(new ProvidedNonNullArguments, ' + { + complicatedArgs { + multipleReqs(req1: 1, req2: 2) + } + } + '); + } + + public function testMultipleArgsReverseOrder() + { + $this->expectPassesRule(new ProvidedNonNullArguments, ' + { + complicatedArgs { + multipleReqs(req2: 2, req1: 1) + } + } + '); + } + + public function testNoArgsOnMultipleOptional() + { + $this->expectPassesRule(new ProvidedNonNullArguments, ' + { + complicatedArgs { + multipleOpts + } + } + '); + } + + public function testOneArgOnMultipleOptional() + { + $this->expectPassesRule(new ProvidedNonNullArguments, ' + { + complicatedArgs { + multipleOpts(opt1: 1) + } + } + '); + } + + public function testSecondArgOnMultipleOptional() + { + $this->expectPassesRule(new ProvidedNonNullArguments, ' + { + complicatedArgs { + multipleOpts(opt2: 1) + } + } + '); + } + + public function testMultipleReqsOnMixedList() + { + $this->expectPassesRule(new ProvidedNonNullArguments, ' + { + complicatedArgs { + multipleOptAndReq(req1: 3, req2: 4) + } + } + '); + } + + public function testMultipleReqsAndOneOptOnMixedList() + { + $this->expectPassesRule(new ProvidedNonNullArguments, ' + { + complicatedArgs { + multipleOptAndReq(req1: 3, req2: 4, opt1: 5) + } + } + '); + } + + public function testAllReqsAndOptsOnMixedList() + { + $this->expectPassesRule(new ProvidedNonNullArguments, ' + { + complicatedArgs { + multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6) + } + } + '); + } + + // Invalid non-nullable value + public function testMissingOneNonNullableArgument() + { + $this->expectFailsRule(new ProvidedNonNullArguments, ' + { + complicatedArgs { + multipleReqs(req2: 2) + } + } + ', [ + $this->missingFieldArg('multipleReqs', 'req1', 'Int!', 4, 13) + ]); + } + + public function testMissingMultipleNonNullableArguments() + { + $this->expectFailsRule(new ProvidedNonNullArguments, ' + { + complicatedArgs { + multipleReqs + } + } + ', [ + $this->missingFieldArg('multipleReqs', 'req1', 'Int!', 4, 13), + $this->missingFieldArg('multipleReqs', 'req2', 'Int!', 4, 13), + ]); + } + + public function testIncorrectValueAndMissingArgument() + { + $this->expectFailsRule(new ProvidedNonNullArguments, ' + { + complicatedArgs { + multipleReqs(req1: "one") + } + } + ', [ + $this->missingFieldArg('multipleReqs', 'req2', 'Int!', 4, 13), + ]); + } + + // Directive arguments + public function testIgnoresUnknownDirectives() + { + $this->expectPassesRule(new ProvidedNonNullArguments, ' + { + dog @unknown + } + '); + } + + public function testWithDirectivesOfValidTypes() + { + $this->expectPassesRule(new ProvidedNonNullArguments, ' + { + dog @include(if: true) { + name + } + human @skip(if: false) { + name + } + } + '); + } + + public function testWithDirectiveWithMissingTypes() + { + $this->expectFailsRule(new ProvidedNonNullArguments, ' + { + dog @include { + name @skip + } + } + ', [ + $this->missingDirectiveArg('include', 'if', 'Boolean!', 3, 15), + $this->missingDirectiveArg('skip', 'if', 'Boolean!', 4, 18) + ]); + } + + private function missingFieldArg($fieldName, $argName, $typeName, $line, $column) + { + return FormattedError::create( + ProvidedNonNullArguments::missingFieldArgMessage($fieldName, $argName, $typeName), + [new SourceLocation($line, $column)] + ); + } + + private function missingDirectiveArg($directiveName, $argName, $typeName, $line, $column) + { + return FormattedError::create( + ProvidedNonNullArguments::missingDirectiveArgMessage($directiveName, $argName, $typeName), + [new SourceLocation($line, $column)] + ); + } +} diff --git a/tests/Validator/ScalarLeafsTest.php b/tests/Validator/ScalarLeafsTest.php index b8fbf5f..1fe67dd 100644 --- a/tests/Validator/ScalarLeafsTest.php +++ b/tests/Validator/ScalarLeafsTest.php @@ -81,10 +81,10 @@ class ScalarLeafsTest extends TestCase { $this->expectFailsRule(new ScalarLeafs, ' fragment scalarSelectionsNotAllowedWithDirectives on Dog { - name @if: true { isAlsoHumanName } + name @include(if: true) { isAlsoHumanName } } ', - [$this->noScalarSubselection('name', 'String', 3, 24)] + [$this->noScalarSubselection('name', 'String', 3, 33)] ); } @@ -92,25 +92,25 @@ class ScalarLeafsTest extends TestCase { $this->expectFailsRule(new ScalarLeafs, ' fragment scalarSelectionsNotAllowedWithDirectivesAndArgs on Dog { - doesKnowCommand(dogCommand: SIT) @if: true { sinceWhen } + doesKnowCommand(dogCommand: SIT) @include(if: true) { sinceWhen } } ', - [$this->noScalarSubselection('doesKnowCommand', 'Boolean', 3, 52)] + [$this->noScalarSubselection('doesKnowCommand', 'Boolean', 3, 61)] ); } private function noScalarSubselection($field, $type, $line, $column) { - return new FormattedError( - Messages::noSubselectionAllowedMessage($field, $type), + return FormattedError::create( + ScalarLeafs::noSubselectionAllowedMessage($field, $type), [new SourceLocation($line, $column)] ); } private function missingObjSubselection($field, $type, $line, $column) { - return new FormattedError( - Messages::requiredSubselectionMessage($field, $type), + return FormattedError::create( + ScalarLeafs::requiredSubselectionMessage($field, $type), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/TestCase.php b/tests/Validator/TestCase.php index 158237a..41a8b1b 100644 --- a/tests/Validator/TestCase.php +++ b/tests/Validator/TestCase.php @@ -20,17 +20,25 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase */ protected function getDefaultSchema() { + $FurColor = null; + $Being = new InterfaceType([ 'name' => 'Being', 'fields' => [ - 'name' => [ 'type' => Type::string() ] + 'name' => [ + 'type' => Type::string(), + 'args' => [ 'surname' => [ 'type' => Type::boolean() ] ] + ] ], ]); $Pet = new InterfaceType([ 'name' => 'Pet', 'fields' => [ - 'name' => [ 'type' => Type::string() ] + 'name' => [ + 'type' => Type::string(), + 'args' => [ 'surname' => [ 'type' => Type::boolean() ] ] + ] ], ]); @@ -45,8 +53,12 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase $Dog = new ObjectType([ 'name' => 'Dog', + 'isTypeOf' => function() {return true;}, 'fields' => [ - 'name' => ['type' => Type::string()], + 'name' => [ + 'type' => Type::string(), + 'args' => [ 'surname' => [ 'type' => Type::boolean() ] ] + ], 'nickname' => ['type' => Type::string()], 'barkVolume' => ['type' => Type::int()], 'barks' => ['type' => Type::boolean()], @@ -66,24 +78,18 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase 'interfaces' => [$Being, $Pet] ]); - $FurColor = new EnumType([ - 'name' => 'FurColor', - 'values' => [ - 'BROWN' => [ 'value' => 0 ], - 'BLACK' => [ 'value' => 1 ], - 'TAN' => [ 'value' => 2 ], - 'SPOTTED' => [ 'value' => 3 ], - ], - ]); - $Cat = new ObjectType([ 'name' => 'Cat', + 'isTypeOf' => function() {return true;}, 'fields' => [ - 'name' => ['type' => Type::string()], + 'name' => [ + 'type' => Type::string(), + 'args' => [ 'surname' => [ 'type' => Type::boolean() ] ] + ], 'nickname' => ['type' => Type::string()], 'meows' => ['type' => Type::boolean()], 'meowVolume' => ['type' => Type::int()], - 'furColor' => ['type' => $FurColor] + 'furColor' => ['type' => function() use (&$FurColor) {return $FurColor;}] ], 'interfaces' => [$Being, $Pet] ]); @@ -106,22 +112,29 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase $Human = $this->humanType = new ObjectType([ 'name' => 'Human', + 'isTypeOf' => function() {return true;}, 'interfaces' => [$Being, $Intelligent], 'fields' => [ 'name' => [ - 'args' => ['surname' => ['type' => Type::boolean()]], - 'type' => Type::string() + 'type' => Type::string(), + 'args' => ['surname' => ['type' => Type::boolean()]] ], 'pets' => ['type' => Type::listOf($Pet)], - 'relatives' => ['type' => function() {return Type::listOf($this->humanType); }] + 'relatives' => ['type' => function() {return Type::listOf($this->humanType); }], + 'iq' => ['type' => Type::int()] ] ]); $Alien = new ObjectType([ 'name' => 'Alien', + 'isTypeOf' => function() {return true;}, 'interfaces' => [$Being, $Intelligent], 'fields' => [ 'iq' => ['type' => Type::int()], + 'name' => [ + 'type' => Type::string(), + 'args' => ['surname' => ['type' => Type::boolean()]] + ], 'numEyes' => ['type' => Type::int()] ] ]); @@ -144,6 +157,16 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase } ]); + $FurColor = new EnumType([ + 'name' => 'FurColor', + 'values' => [ + 'BROWN' => [ 'value' => 0 ], + 'BLACK' => [ 'value' => 1 ], + 'TAN' => [ 'value' => 2 ], + 'SPOTTED' => [ 'value' => 3 ], + ], + ]); + $ComplexInput = new InputObjectType([ 'name' => 'ComplexInput', 'fields' => [ @@ -260,19 +283,20 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase function expectValid($schema, $rules, $queryString) { $this->assertEquals( - ['isValid' => true, 'errors' => null], - DocumentValidator::validate($schema, Parser::parse($queryString), $rules) + [], + DocumentValidator::validate($schema, Parser::parse($queryString), $rules), + 'Should validate' ); } - function expectInvalid($schema, $rules, $queryString, $errors) + function expectInvalid($schema, $rules, $queryString, $expectedErrors) { - $result = DocumentValidator::validate($schema, Parser::parse($queryString), $rules); + $errors = DocumentValidator::validate($schema, Parser::parse($queryString), $rules); - $this->assertEquals(false, $result['isValid'], 'GraphQL should not validate'); - $this->assertEquals($errors, $result['errors']); + $this->assertNotEmpty($errors, 'GraphQL should not validate'); + $this->assertEquals($expectedErrors, array_map(['GraphQL\Error', 'formatError'], $errors)); - return $result; + return $errors; } function expectPassesRule($rule, $queryString) diff --git a/tests/Validator/VariablesAreInputTypesTest.php b/tests/Validator/VariablesAreInputTypesTest.php index 4a00571..04b76cd 100644 --- a/tests/Validator/VariablesAreInputTypesTest.php +++ b/tests/Validator/VariablesAreInputTypesTest.php @@ -20,20 +20,20 @@ class VariablesAreInputTypesTest extends TestCase public function testOutputTypesAreInvalid() { $this->expectFailsRule(new VariablesAreInputTypes, ' - query Foo($a: Dog, $b: [[DogOrCat!]]!, $c: Pet) { + query Foo($a: Dog, $b: [[CatOrDog!]]!, $c: Pet) { field(a: $a, b: $b, c: $c) } ', [ - new FormattedError( - Messages::nonInputTypeOnVarMessage('a', 'Dog'), + FormattedError::create( + VariablesAreInputTypes::nonInputTypeOnVarMessage('a', 'Dog'), [new SourceLocation(2, 21)] ), - new FormattedError( - Messages::nonInputTypeOnVarMessage('b', '[[DogOrCat!]]!'), + FormattedError::create( + VariablesAreInputTypes::nonInputTypeOnVarMessage('b', '[[CatOrDog!]]!'), [new SourceLocation(2, 30)] ), - new FormattedError( - Messages::nonInputTypeOnVarMessage('c', 'Pet'), + FormattedError::create( + VariablesAreInputTypes::nonInputTypeOnVarMessage('c', 'Pet'), [new SourceLocation(2, 50)] ) ] diff --git a/tests/Validator/VariablesInAllowedPositionTest.php b/tests/Validator/VariablesInAllowedPositionTest.php index 126c012..c4accb4 100644 --- a/tests/Validator/VariablesInAllowedPositionTest.php +++ b/tests/Validator/VariablesInAllowedPositionTest.php @@ -177,7 +177,7 @@ class VariablesInAllowedPositionTest extends TestCase $this->expectPassesRule(new VariablesInAllowedPosition, ' query Query($boolVar: Boolean!) { - dog @if: $boolVar + dog @include(if: $boolVar) } '); } @@ -188,7 +188,7 @@ class VariablesInAllowedPositionTest extends TestCase $this->expectPassesRule(new VariablesInAllowedPosition, ' query Query($boolVar: Boolean = false) { - dog @if: $boolVar + dog @include(if: $boolVar) } '); } @@ -204,7 +204,7 @@ class VariablesInAllowedPositionTest extends TestCase } } ', [ - new FormattedError( + FormattedError::create( Messages::badVarPosMessage('intArg', 'Int', 'Int!'), [new SourceLocation(5, 45)] ) @@ -226,7 +226,7 @@ class VariablesInAllowedPositionTest extends TestCase } } ', [ - new FormattedError( + FormattedError::create( Messages::badVarPosMessage('intArg', 'Int', 'Int!'), [new SourceLocation(3, 43)] ) @@ -252,7 +252,7 @@ class VariablesInAllowedPositionTest extends TestCase } } ', [ - new FormattedError( + FormattedError::create( Messages::badVarPosMessage('intArg', 'Int', 'Int!'), [new SourceLocation(7,43)] ) @@ -270,7 +270,7 @@ class VariablesInAllowedPositionTest extends TestCase } } ', [ - new FormattedError( + FormattedError::create( Messages::badVarPosMessage('stringVar', 'String', 'Boolean'), [new SourceLocation(5,39)] ) @@ -288,7 +288,7 @@ class VariablesInAllowedPositionTest extends TestCase } } ', [ - new FormattedError( + FormattedError::create( Messages::badVarPosMessage('stringVar', 'String', '[String]'), [new SourceLocation(5,45)] ) @@ -301,12 +301,12 @@ class VariablesInAllowedPositionTest extends TestCase $this->expectFailsRule(new VariablesInAllowedPosition, ' query Query($boolVar: Boolean) { - dog @if: $boolVar + dog @include(if: $boolVar) } ', [ - new FormattedError( + FormattedError::create( Messages::badVarPosMessage('boolVar', 'Boolean', 'Boolean!'), - [new SourceLocation(4,18)] + [new SourceLocation(4,26)] ) ]); } @@ -317,12 +317,12 @@ class VariablesInAllowedPositionTest extends TestCase $this->expectFailsRule(new VariablesInAllowedPosition, ' query Query($stringVar: String) { - dog @if: $stringVar + dog @include(if: $stringVar) } ', [ - new FormattedError( + FormattedError::create( Messages::badVarPosMessage('stringVar', 'String', 'Boolean!'), - [new SourceLocation(4,18)] + [new SourceLocation(4,26)] ) ]); }