Updated to latest version of graphql-js

This commit is contained in:
vladar 2015-08-17 20:01:55 +06:00
parent 022c962942
commit 841d6ab851
88 changed files with 3227 additions and 1669 deletions

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Executor;
use GraphQL\Error;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Schema;
@ -25,7 +26,7 @@ class ExecutionContext
/**
* @var
*/
public $root;
public $rootValue;
/**
* @var OperationDefinition
@ -35,7 +36,7 @@ class ExecutionContext
/**
* @var array
*/
public $variables;
public $variableValues;
/**
* @var array
@ -46,13 +47,13 @@ class ExecutionContext
{
$this->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;

View File

@ -0,0 +1,38 @@
<?php
namespace GraphQL\Executor;
use GraphQL\Error;
class ExecutionResult
{
/**
* @var array
*/
public $data;
/**
* @var Error[]
*/
public $errors;
/**
* @param array $data
* @param array $errors
*/
public function __construct(array $data = null, array $errors = [])
{
$this->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;
}
}

View File

@ -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<Field>*/ $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<Field> */ $fieldASTs, &$result)
private static function completeValue(ExecutionContext $exeContext, Type $returnType,/* Array<Field> */ $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;
}
}

View File

@ -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<VariableDefinition> */ $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<GraphQLFieldArgument>*/ $argDefs, /*Array<Argument>*/ $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<Directive> */ $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;
}
return self::coerceValueAST($directiveDef->type, $directiveAST->value, $variables);
if ($valueAST instanceof Variable) {
$variableName = $valueAST->name->value;
if (!$variables || !isset($variables[$variableName])) {
return null;
}
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) {
$fields = $type->getFields();
foreach ($fields as $fieldName => $field) {
/** @var FieldDefinition $field */
if (!self::isValidValue($field->getType(), isset($value[$fieldName]) ? $value[$fieldName] : null)) {
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(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;
$fieldValue = self::coerceValue($field->getType(), isset($value[$fieldName]) ? $value[$fieldName] : null);
if (null === $fieldValue) {
$fieldValue = $field->defaultValue;
}
if (null !== $fieldValue) {
$obj[$fieldName] = $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);
}
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);
}
}

View File

@ -11,25 +11,25 @@ class GraphQL
/**
* @param Schema $schema
* @param $requestString
* @param mixed $rootObject
* @param mixed $rootValue
* @param array <string, string>|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)]];
}
}
}

View File

@ -1,7 +1,7 @@
<?php
namespace GraphQL\Language\AST;
class Argument extends NamedType
class Argument extends Node
{
public $kind = Node::ARGUMENT;
@ -9,4 +9,9 @@ class Argument extends NamedType
* @var Value
*/
public $value;
/**
* @var Name
*/
public $name;
}

View File

@ -1,10 +1,15 @@
<?php
namespace GraphQL\Language\AST;
class Field extends NamedType
class Field extends Node
{
public $kind = Node::FIELD;
/**
* @var Name
*/
public $name;
/**
* @var Name|null
*/

View File

@ -2,10 +2,15 @@
namespace GraphQL\Language\AST;
class FragmentDefinition extends NamedType implements Definition
class FragmentDefinition extends Node implements Definition
{
public $kind = Node::FRAGMENT_DEFINITION;
/**
* @var Name
*/
public $name;
/**
* @var NamedType
*/

View File

@ -1,10 +1,15 @@
<?php
namespace GraphQL\Language\AST;
class FragmentSpread extends NamedType
class FragmentSpread extends Node
{
public $kind = Node::FRAGMENT_SPREAD;
/**
* @var Name
*/
public $name;
/**
* @var array<Directive>
*/

View File

@ -1,7 +1,7 @@
<?php
namespace GraphQL\Language\AST;
class NamedType extends Node
class NamedType extends Node implements Type
{
public $kind = Node::NAMED_TYPE;

View File

@ -2,10 +2,15 @@
namespace GraphQL\Language\AST;
class ObjectField extends NamedType
class ObjectField extends Node
{
public $kind = Node::OBJECT_FIELD;
/**
* @var Name
*/
public $name;
/**
* @var Value
*/

View File

@ -1,13 +1,18 @@
<?php
namespace GraphQL\Language\AST;
class OperationDefinition extends NamedType implements Definition
class OperationDefinition extends Node implements Definition
{
/**
* @var string
*/
public $kind = Node::OPERATION_DEFINITION;
/**
* @var Name
*/
public $name;
/**
* @var string (oneOf 'query', 'mutation'))
*/

View File

@ -1,7 +1,12 @@
<?php
namespace GraphQL\Language\AST;
class Variable extends NamedType
class Variable extends Node
{
public $kind = Node::VARIABLE;
/**
* @var Name
*/
public $name;
}

View File

@ -2,7 +2,12 @@
namespace GraphQL;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
@ -24,6 +29,99 @@ class Schema
Utils::invariant($querySchema || $mutationSchema, "Either query or mutation type must be set");
$this->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);
}
$nestedTypes[] = $field->getType();
}
}

View File

@ -13,4 +13,15 @@ GraphQLUnionType;
* @return array<ObjectType>
*/
public function getPossibleTypes();
/**
* @return ObjectType
*/
public function getObjectType($value, ResolveInfo $info);
/**
* @param Type $type
* @return bool
*/
public function isPossibleType(Type $type);
}

View File

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

View File

@ -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([
'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',
'description' => 'Directs the executor to omit this field if the argument provided is false.',
'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.',
'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

View File

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

View File

@ -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
{
/**

View File

@ -1,6 +1,8 @@
<?php
namespace GraphQL\Type\Definition;
use GraphQL\Utils;
class FieldDefinition
{
/**
@ -98,12 +100,28 @@ class FieldDefinition
$this->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;

View File

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

View File

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

View File

@ -32,7 +32,7 @@ class InputObjectType extends Type implements InputType
}
/**
* @return array<InputObjectField>
* @return InputObjectField[]
*/
public function getFields()
{

View File

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

View File

@ -35,11 +35,11 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
* implementation for Interface types.
*
* @param ObjectType $impl
* @param array<InterfaceType> $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);
}
}

View File

@ -45,7 +45,7 @@ class ObjectType extends Type implements OutputType, CompositeType
/**
* @var array<Field>
*/
private $_fields = [];
private $_fields;
/**
* @var array<InterfaceType>
@ -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->_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;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace GraphQL\Type\Definition;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\FragmentDefinition;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Schema;
use GraphQL\Utils;
class ResolveInfo
{
/**
* @var string
*/
public $fieldName;
/**
* @var Field[]
*/
public $fieldASTs;
/**
* @var OutputType
*/
public $returnType;
/**
* @var Type|CompositeType
*/
public $parentType;
/**
* @var Schema
*/
public $schema;
/**
* @var array<fragmentName, FragmentDefinition>
*/
public $fragments;
/**
* @var mixed
*/
public $rootValue;
/**
* @var OperationDefinition
*/
public $operation;
/**
* @var array<variableName, mixed>
*/
public $variableValues;
public function __construct(array $values)
{
Utils::assign($this, $values);
}
}

View File

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

View File

@ -1,26 +0,0 @@
<?php
namespace GraphQL\Type\Definition;
class ScalarTypeConfig
{
/**
* @var string
*/
public $name;
/**
* @var string|null
*/
public $description;
/**
* @var \Closure
*/
public $coerce;
/**
* @var \Closure
*/
public $coerceLiteral;
}

View File

@ -7,7 +7,12 @@ class StringType extends ScalarType
{
public $name = Type::STRING;
public function coerce($value)
public function serialize($value)
{
return $this->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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.");

View File

@ -16,53 +16,23 @@ 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();
}
$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];
Node::ARGUMENT => function(Argument $argAST) use ($context) {
$argDef = $context->getArgument();
if ($argDef && !DocumentValidator::isValidLiteralValue($argAST->value, $argDef->getType())) {
$errors[] = new Error(
Messages::badValueMessage(
$argAST->name->value,
$argDef->getType(),
Printer::doPrint($argAST->value)
),
return new Error(
self::badValueMessage($argAST->name->value, $argDef->getType(), Printer::doPrint($argAST->value)),
[$argAST->value]
);
}
}
return !empty($errors) ? $errors : null;
}
];
}
}

View File

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

View File

@ -11,29 +11,59 @@ 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) {
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) {
$argDef = null;
$fieldArgDef = null;
foreach ($fieldDef->args as $arg) {
if ($arg->name === $node->name->value) {
$argDef = $arg;
$fieldArgDef = $arg;
break;
}
}
if (!$argDef) {
if (!$fieldArgDef) {
$parentType = $context->getParentType();
Utils::invariant($parentType);
return new Error(
Messages::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name),
self::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]
);
}
}
}
}
];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,6 +64,8 @@ class NoUnusedFragments
foreach ($spreads as $fragName => $fragment) {
if (empty($fragmentNameUsed[$fragName])) {
$fragmentNameUsed[$fragName] = true;
if (isset($fragAdjacencies->{$fragName})) {
$this->reduceSpreadFragments(
$fragAdjacencies->{$fragName},
$fragmentNameUsed,
@ -68,3 +75,4 @@ class NoUnusedFragments
}
}
}
}

View File

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

View File

@ -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<Argument | Directive> $pairs1
* @param Array<Argument | Directive> $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;
}
}

View File

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

View File

@ -0,0 +1,87 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error;
use GraphQL\Language\AST\Directive;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\Node;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Utils;
use GraphQL\Validator\ValidationContext;
class ProvidedNonNullArguments
{
static function missingFieldArgMessage($fieldName, $argName, $type)
{
return "Field \"$fieldName\" argument \"$argName\" of type \"$type\" is required but not provided.";
}
static function missingDirectiveArgMessage($directiveName, $argName, $type)
{
return "Directive \"@$directiveName\" argument \"$argName\" of type \"$type\" is required but not provided.";
}
public function __invoke(ValidationContext $context)
{
return [
Node::FIELD => [
'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;
}
}
]
];
}
}

View File

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

View File

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

View File

@ -113,4 +113,14 @@ class ValidationContext
{
return $this->_typeInfo->getFieldDef();
}
function getDirective()
{
return $this->_typeInfo->getDirective();
}
function getArgument()
{
return $this->_typeInfo->getArgument();
}
}

View File

@ -0,0 +1,244 @@
<?php
namespace GraphQL\Executor;
require_once __DIR__ . '/TestClasses.php';
use GraphQL\Language\Parser;
use GraphQL\Schema;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
class AbstractTest extends \PHPUnit_Framework_TestCase
{
// Execute: Handles execution of abstract types
public function testIsTypeOfUsedToResolveRuntimeTypeForInterface()
{
// isTypeOf used to resolve runtime type for Interface
$petType = new InterfaceType([
'name' => '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());
}
}

View File

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

View File

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

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Executor;
use GraphQL\Error;
use GraphQL\FormattedError;
use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;
@ -132,7 +133,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
]);
$schema = new Schema($dataType);
$this->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 } ] }
]);
});
*/
}
}

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Executor;
use GraphQL\Error;
use GraphQL\FormattedError;
use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;
@ -24,7 +25,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
$ast = Parser::parse($doc);
$expected = ['data' => ['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());
}

View File

@ -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()

View File

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

View File

@ -0,0 +1,78 @@
<?php
namespace GraphQL\Executor;
use GraphQL\Type\Definition\ScalarType;
class Dog
{
function __construct($name, $woofs)
{
$this->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;
}
}

View File

@ -1,6 +1,8 @@
<?php
namespace GraphQL\Executor;
require_once __DIR__ . '/TestClasses.php';
use GraphQL\Language\Parser;
use GraphQL\Schema;
use GraphQL\Type\Definition\Config;
@ -31,7 +33,7 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
'interfaces' => [$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());
}
}

View File

@ -1,17 +1,21 @@
<?php
namespace GraphQL\Executor;
require_once __DIR__ . '/TestClasses.php';
use GraphQL\Error;
use GraphQL\FormattedError;
use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;
use GraphQL\Schema;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
class InputObjectTest extends \PHPUnit_Framework_TestCase
class VariablesTest extends \PHPUnit_Framework_TestCase
{
// Execute: Handles input objects
// Execute: Handles inputs
// Handles objects and nullability
public function testUsingInlineStructs()
@ -29,9 +33,9 @@ class InputObjectTest extends \PHPUnit_Framework_TestCase
'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());
// 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(
$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(
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, Executor::execute($schema, null, $ast, null, $params));
$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(
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, Executor::execute($this->schema(), null, $ast));
);
$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(
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, Executor::execute($this->schema(), null, $ast, null, ['value' => null]));
);
$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(
$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(
$expected = FormattedError::create(
'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']]));
);
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(
$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 {
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(
$expected = FormattedError::create(
'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']]));
);
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;
}
],
]

View File

@ -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' => [

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,63 +0,0 @@
<?php
namespace GraphQL\Type;
use GraphQL\Type\Definition\Type;
class ScalarCoercionTest extends \PHPUnit_Framework_TestCase
{
public function testCoercesOutputInt()
{
$intType = Type::int();
$this->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'?
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace GraphQL\Type;
use GraphQL\Type\Definition\Type;
class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
{
public function testCoercesOutputInt()
{
$intType = Type::int();
$this->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'?
}
}

View File

@ -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!'
);
*/
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,217 @@
<?php
namespace GraphQL\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
class ProvidedNonNullArgumentsTest extends TestCase
{
// Validate: Provided required arguments
public function testIgnoresUnknownArguments()
{
// ignores unknown arguments
$this->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)]
);
}
}

View File

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

View File

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

View File

@ -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)]
)
]

View File

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