<?php

declare(strict_types=1);

namespace GraphQL\Language;

use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\EnumTypeExtensionNode;
use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\EnumValueNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FloatValueNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
use GraphQL\Language\AST\IntValueNode;
use GraphQL\Language\AST\ListTypeNode;
use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\ObjectFieldNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\OperationTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeExtensionNode;
use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\AST\SchemaTypeExtensionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\AST\UnionTypeExtensionNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Utils\Utils;
use function count;
use function implode;
use function json_encode;
use function preg_replace;
use function sprintf;
use function str_replace;
use function strpos;

/**
 * Prints AST to string. Capable of printing GraphQL queries and Type definition language.
 * Useful for pretty-printing queries or printing back AST for logging, documentation, etc.
 *
 * Usage example:
 *
 * ```php
 * $query = 'query myQuery {someField}';
 * $ast = GraphQL\Language\Parser::parse($query);
 * $printed = GraphQL\Language\Printer::doPrint($ast);
 * ```
 */
class Printer
{
    /**
     * Prints AST to string. Capable of printing GraphQL queries and Type definition language.
     *
     * @param Node $ast
     *
     * @return string
     *
     * @api
     */
    public static function doPrint($ast)
    {
        static $instance;
        $instance = $instance ?: new static();

        return $instance->printAST($ast);
    }

    protected function __construct()
    {
    }

    public function printAST($ast)
    {
        return Visitor::visit(
            $ast,
            [
                'leave' => [
                    NodeKind::NAME => static function (Node $node) {
                        return '' . $node->value;
                    },

                    NodeKind::VARIABLE => static function ($node) {
                        return '$' . $node->name;
                    },

                    NodeKind::DOCUMENT => function (DocumentNode $node) {
                        return $this->join($node->definitions, "\n\n") . "\n";
                    },

                    NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) {
                        $op           = $node->operation;
                        $name         = $node->name;
                        $varDefs      = $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')');
                        $directives   = $this->join($node->directives, ' ');
                        $selectionSet = $node->selectionSet;

                        // Anonymous queries with no directives or variable definitions can use
                        // the query short form.
                        return ! $name && ! $directives && ! $varDefs && $op === 'query'
                            ? $selectionSet
                            : $this->join([$op, $this->join([$name, $varDefs]), $directives, $selectionSet], ' ');
                    },

                    NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) {
                        return $node->variable . ': ' . $node->type . $this->wrap(' = ', $node->defaultValue);
                    },

                    NodeKind::SELECTION_SET => function (SelectionSetNode $node) {
                        return $this->block($node->selections);
                    },

                    NodeKind::FIELD => function (FieldNode $node) {
                        return $this->join(
                            [
                                $this->wrap('', $node->alias, ': ') . $node->name . $this->wrap(
                                    '(',
                                    $this->join($node->arguments, ', '),
                                    ')'
                                ),
                                $this->join($node->directives, ' '),
                                $node->selectionSet,
                            ],
                            ' '
                        );
                    },

                    NodeKind::ARGUMENT => static function (ArgumentNode $node) {
                        return $node->name . ': ' . $node->value;
                    },

                    NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) {
                        return '...' . $node->name . $this->wrap(' ', $this->join($node->directives, ' '));
                    },

                    NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) {
                        return $this->join(
                            [
                                '...',
                                $this->wrap('on ', $node->typeCondition),
                                $this->join($node->directives, ' '),
                                $node->selectionSet,
                            ],
                            ' '
                        );
                    },

                    NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) {
                        // Note: fragment variable definitions are experimental and may be changed or removed in the future.
                        return sprintf('fragment %s', $node->name)
                            . $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')')
                            . sprintf(' on %s ', $node->typeCondition)
                            . $this->wrap('', $this->join($node->directives, ' '), ' ')
                            . $node->selectionSet;
                    },

                    NodeKind::INT => static function (IntValueNode $node) {
                        return $node->value;
                    },

                    NodeKind::FLOAT => static function (FloatValueNode $node) {
                        return $node->value;
                    },

                    NodeKind::STRING => function (StringValueNode $node, $key) {
                        if ($node->block) {
                            return $this->printBlockString($node->value, $key === 'description');
                        }

                        return json_encode($node->value);
                    },

                    NodeKind::BOOLEAN => static function (BooleanValueNode $node) {
                        return $node->value ? 'true' : 'false';
                    },

                    NodeKind::NULL => static function (NullValueNode $node) {
                        return 'null';
                    },

                    NodeKind::ENUM => static function (EnumValueNode $node) {
                        return $node->value;
                    },

                    NodeKind::LST => function (ListValueNode $node) {
                        return '[' . $this->join($node->values, ', ') . ']';
                    },

                    NodeKind::OBJECT => function (ObjectValueNode $node) {
                        return '{' . $this->join($node->fields, ', ') . '}';
                    },

                    NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) {
                        return $node->name . ': ' . $node->value;
                    },

                    NodeKind::DIRECTIVE => function (DirectiveNode $node) {
                        return '@' . $node->name . $this->wrap('(', $this->join($node->arguments, ', '), ')');
                    },

                    NodeKind::NAMED_TYPE => static function (NamedTypeNode $node) {
                        return $node->name;
                    },

                    NodeKind::LIST_TYPE => static function (ListTypeNode $node) {
                        return '[' . $node->type . ']';
                    },

                    NodeKind::NON_NULL_TYPE => static function (NonNullTypeNode $node) {
                        return $node->type . '!';
                    },

                    NodeKind::SCHEMA_DEFINITION => function (SchemaDefinitionNode $def) {
                        return $this->join(
                            [
                                'schema',
                                $this->join($def->directives, ' '),
                                $this->block($def->operationTypes),
                            ],
                            ' '
                        );
                    },

                    NodeKind::OPERATION_TYPE_DEFINITION => static function (OperationTypeDefinitionNode $def) {
                        return $def->operation . ': ' . $def->type;
                    },

                    NodeKind::SCALAR_TYPE_DEFINITION => $this->addDescription(function (ScalarTypeDefinitionNode $def) {
                        return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' ');
                    }),

                    NodeKind::OBJECT_TYPE_DEFINITION => $this->addDescription(function (ObjectTypeDefinitionNode $def) {
                        return $this->join(
                            [
                                'type',
                                $def->name,
                                $this->wrap('implements ', $this->join($def->interfaces, ' & ')),
                                $this->join($def->directives, ' '),
                                $this->block($def->fields),
                            ],
                            ' '
                        );
                    }),

                    NodeKind::FIELD_DEFINITION => $this->addDescription(function (FieldDefinitionNode $def) {
                        $noIndent = Utils::every($def->arguments, static function (string $arg) {
                            return strpos($arg, "\n") === false;
                        });

                        return $def->name
                            . ($noIndent
                                ? $this->wrap('(', $this->join($def->arguments, ', '), ')')
                                : $this->wrap("(\n", $this->indent($this->join($def->arguments, "\n")), "\n)"))
                            . ': ' . $def->type
                            . $this->wrap(' ', $this->join($def->directives, ' '));
                    }),

                    NodeKind::INPUT_VALUE_DEFINITION => $this->addDescription(function (InputValueDefinitionNode $def) {
                        return $this->join(
                            [
                                $def->name . ': ' . $def->type,
                                $this->wrap('= ', $def->defaultValue),
                                $this->join($def->directives, ' '),
                            ],
                            ' '
                        );
                    }),

                    NodeKind::INTERFACE_TYPE_DEFINITION => $this->addDescription(
                        function (InterfaceTypeDefinitionNode $def) {
                            return $this->join(
                                [
                                    'interface',
                                    $def->name,
                                    $this->join($def->directives, ' '),
                                    $this->block($def->fields),
                                ],
                                ' '
                            );
                        }
                    ),

                    NodeKind::UNION_TYPE_DEFINITION => $this->addDescription(function (UnionTypeDefinitionNode $def) {
                        return $this->join(
                            [
                                'union',
                                $def->name,
                                $this->join($def->directives, ' '),
                                $def->types
                                    ? '= ' . $this->join($def->types, ' | ')
                                    : '',
                            ],
                            ' '
                        );
                    }),

                    NodeKind::ENUM_TYPE_DEFINITION => $this->addDescription(function (EnumTypeDefinitionNode $def) {
                        return $this->join(
                            [
                                'enum',
                                $def->name,
                                $this->join($def->directives, ' '),
                                $this->block($def->values),
                            ],
                            ' '
                        );
                    }),

                    NodeKind::ENUM_VALUE_DEFINITION => $this->addDescription(function (EnumValueDefinitionNode $def) {
                        return $this->join([$def->name, $this->join($def->directives, ' ')], ' ');
                    }),

                    NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $this->addDescription(function (
                        InputObjectTypeDefinitionNode $def
                    ) {
                        return $this->join(
                            [
                                'input',
                                $def->name,
                                $this->join($def->directives, ' '),
                                $this->block($def->fields),
                            ],
                            ' '
                        );
                    }),

                    NodeKind::SCHEMA_EXTENSION => function (SchemaTypeExtensionNode $def) {
                        return $this->join(
                            [
                                'extend schema',
                                $this->join($def->directives, ' '),
                                $this->block($def->operationTypes),
                            ],
                            ' '
                        );
                    },

                    NodeKind::SCALAR_TYPE_EXTENSION => function (ScalarTypeExtensionNode $def) {
                        return $this->join(
                            [
                                'extend scalar',
                                $def->name,
                                $this->join($def->directives, ' '),
                            ],
                            ' '
                        );
                    },

                    NodeKind::OBJECT_TYPE_EXTENSION => function (ObjectTypeExtensionNode $def) {
                        return $this->join(
                            [
                                'extend type',
                                $def->name,
                                $this->wrap('implements ', $this->join($def->interfaces, ' & ')),
                                $this->join($def->directives, ' '),
                                $this->block($def->fields),
                            ],
                            ' '
                        );
                    },

                    NodeKind::INTERFACE_TYPE_EXTENSION => function (InterfaceTypeExtensionNode $def) {
                        return $this->join(
                            [
                                'extend interface',
                                $def->name,
                                $this->join($def->directives, ' '),
                                $this->block($def->fields),
                            ],
                            ' '
                        );
                    },

                    NodeKind::UNION_TYPE_EXTENSION => function (UnionTypeExtensionNode $def) {
                        return $this->join(
                            [
                                'extend union',
                                $def->name,
                                $this->join($def->directives, ' '),
                                $def->types
                                    ? '= ' . $this->join($def->types, ' | ')
                                    : '',
                            ],
                            ' '
                        );
                    },

                    NodeKind::ENUM_TYPE_EXTENSION => function (EnumTypeExtensionNode $def) {
                        return $this->join(
                            [
                                'extend enum',
                                $def->name,
                                $this->join($def->directives, ' '),
                                $this->block($def->values),
                            ],
                            ' '
                        );
                    },

                    NodeKind::INPUT_OBJECT_TYPE_EXTENSION => function (InputObjectTypeExtensionNode $def) {
                        return $this->join(
                            [
                                'extend input',
                                $def->name,
                                $this->join($def->directives, ' '),
                                $this->block($def->fields),
                            ],
                            ' '
                        );
                    },

                    NodeKind::DIRECTIVE_DEFINITION => $this->addDescription(function (DirectiveDefinitionNode $def) {
                        $noIndent = Utils::every($def->arguments, static function (string $arg) {
                            return strpos($arg, "\n") === false;
                        });

                        return 'directive @'
                            . $def->name
                            . ($noIndent
                                ? $this->wrap('(', $this->join($def->arguments, ', '), ')')
                                : $this->wrap("(\n", $this->indent($this->join($def->arguments, "\n")), "\n"))
                            . ' on ' . $this->join($def->locations, ' | ');
                    }),
                ],
            ]
        );
    }

    public function addDescription(callable $cb)
    {
        return function ($node) use ($cb) {
            return $this->join([$node->description, $cb($node)], "\n");
        };
    }

    /**
     * If maybeString is not null or empty, then wrap with start and end, otherwise
     * print an empty string.
     */
    public function wrap($start, $maybeString, $end = '')
    {
        return $maybeString ? ($start . $maybeString . $end) : '';
    }

    /**
     * Given array, print each item on its own line, wrapped in an
     * indented "{ }" block.
     */
    public function block($array)
    {
        return $array && $this->length($array)
            ? "{\n" . $this->indent($this->join($array, "\n")) . "\n}"
            : '';
    }

    public function indent($maybeString)
    {
        return $maybeString ? '  ' . str_replace("\n", "\n  ", $maybeString) : '';
    }

    public function manyList($start, $list, $separator, $end)
    {
        return $this->length($list) === 0 ? null : ($start . $this->join($list, $separator) . $end);
    }

    public function length($maybeArray)
    {
        return $maybeArray ? count($maybeArray) : 0;
    }

    public function join($maybeArray, $separator = '')
    {
        return $maybeArray
            ? implode(
                $separator,
                Utils::filter(
                    $maybeArray,
                    static function ($x) {
                        return (bool) $x;
                    }
                )
            )
            : '';
    }

    /**
     * Print a block string in the indented block form by adding a leading and
     * trailing blank line. However, if a block string starts with whitespace and is
     * a single-line, adding a leading blank line would strip that whitespace.
     */
    private function printBlockString($value, $isDescription)
    {
        $escaped = str_replace('"""', '\\"""', $value);

        return ($value[0] === ' ' || $value[0] === "\t") && strpos($value, "\n") === false
            ? ('"""' . preg_replace('/"$/', "\"\n", $escaped) . '"""')
            : ('"""' . "\n" . ($isDescription ? $escaped : $this->indent($escaped)) . "\n" . '"""');
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Error;

use Countable;
use ErrorException;
use Exception;
use GraphQL\Language\AST\Node;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType;
use GraphQL\Utils\Utils;
use Throwable;
use function addcslashes;
use function array_filter;
use function array_intersect_key;
use function array_map;
use function array_merge;
use function array_shift;
use function count;
use function get_class;
use function gettype;
use function implode;
use function is_array;
use function is_bool;
use function is_object;
use function is_scalar;
use function is_string;
use function mb_strlen;
use function preg_split;
use function sprintf;
use function str_repeat;
use function strlen;

/**
 * This class is used for [default error formatting](error-handling.md).
 * It converts PHP exceptions to [spec-compliant errors](https://facebook.github.io/graphql/#sec-Errors)
 * and provides tools for error debugging.
 */
class FormattedError
{
    /** @var string */
    private static $internalErrorMessage = 'Internal server error';

    /**
     * Set default error message for internal errors formatted using createFormattedError().
     * This value can be overridden by passing 3rd argument to `createFormattedError()`.
     *
     * @param string $msg
     *
     * @api
     */
    public static function setInternalErrorMessage($msg)
    {
        self::$internalErrorMessage = $msg;
    }

    /**
     * Prints a GraphQLError to a string, representing useful location information
     * about the error's position in the source.
     *
     * @return string
     */
    public static function printError(Error $error)
    {
        $printedLocations = [];
        if ($error->nodes) {
            /** @var Node $node */
            foreach ($error->nodes as $node) {
                if (! $node->loc) {
                    continue;
                }

                if ($node->loc->source === null) {
                    continue;
                }

                $printedLocations[] = self::highlightSourceAtLocation(
                    $node->loc->source,
                    $node->loc->source->getLocation($node->loc->start)
                );
            }
        } elseif ($error->getSource() && $error->getLocations()) {
            $source = $error->getSource();
            foreach ($error->getLocations() as $location) {
                $printedLocations[] = self::highlightSourceAtLocation($source, $location);
            }
        }

        return ! $printedLocations
            ? $error->getMessage()
            : implode("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n";
    }

    /**
     * Render a helpful description of the location of the error in the GraphQL
     * Source document.
     *
     * @return string
     */
    private static function highlightSourceAtLocation(Source $source, SourceLocation $location)
    {
        $line          = $location->line;
        $lineOffset    = $source->locationOffset->line - 1;
        $columnOffset  = self::getColumnOffset($source, $location);
        $contextLine   = $line + $lineOffset;
        $contextColumn = $location->column + $columnOffset;
        $prevLineNum   = (string) ($contextLine - 1);
        $lineNum       = (string) $contextLine;
        $nextLineNum   = (string) ($contextLine + 1);
        $padLen        = strlen($nextLineNum);
        $lines         = preg_split('/\r\n|[\n\r]/', $source->body);

        $lines[0] = self::whitespace($source->locationOffset->column - 1) . $lines[0];

        $outputLines = [
            sprintf('%s (%s:%s)', $source->name, $contextLine, $contextColumn),
            $line >= 2 ? (self::lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null,
            self::lpad($padLen, $lineNum) . ': ' . $lines[$line - 1],
            self::whitespace(2 + $padLen + $contextColumn - 1) . '^',
            $line < count($lines) ? self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
        ];

        return implode("\n", array_filter($outputLines));
    }

    /**
     * @return int
     */
    private static function getColumnOffset(Source $source, SourceLocation $location)
    {
        return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
    }

    /**
     * @param int $len
     *
     * @return string
     */
    private static function whitespace($len)
    {
        return str_repeat(' ', $len);
    }

    /**
     * @param int $len
     *
     * @return string
     */
    private static function lpad($len, $str)
    {
        return self::whitespace($len - mb_strlen($str)) . $str;
    }

    /**
     * Standard GraphQL error formatter. Converts any exception to array
     * conforming to GraphQL spec.
     *
     * This method only exposes exception message when exception implements ClientAware interface
     * (or when debug flags are passed).
     *
     * For a list of available debug flags see GraphQL\Error\Debug constants.
     *
     * @param Throwable $e
     * @param bool|int  $debug
     * @param string    $internalErrorMessage
     *
     * @return mixed[]
     *
     * @throws Throwable
     *
     * @api
     */
    public static function createFromException($e, $debug = false, $internalErrorMessage = null)
    {
        Utils::invariant(
            $e instanceof Exception || $e instanceof Throwable,
            'Expected exception, got %s',
            Utils::getVariableType($e)
        );

        $internalErrorMessage = $internalErrorMessage ?: self::$internalErrorMessage;

        if ($e instanceof ClientAware) {
            $formattedError = [
                'message'  => $e->isClientSafe() ? $e->getMessage() : $internalErrorMessage,
                'extensions' => [
                    'category' => $e->getCategory(),
                ],
            ];
        } else {
            $formattedError = [
                'message'  => $internalErrorMessage,
                'extensions' => [
                    'category' => Error::CATEGORY_INTERNAL,
                ],
            ];
        }

        if ($e instanceof Error) {
            $locations = Utils::map(
                $e->getLocations(),
                static function (SourceLocation $loc) {
                    return $loc->toSerializableArray();
                }
            );
            if (! empty($locations)) {
                $formattedError['locations'] = $locations;
            }
            if (! empty($e->path)) {
                $formattedError['path'] = $e->path;
            }
            if (! empty($e->getExtensions())) {
                $formattedError['extensions'] = $e->getExtensions() + $formattedError['extensions'];
            }
        }

        if ($debug) {
            $formattedError = self::addDebugEntries($formattedError, $e, $debug);
        }

        return $formattedError;
    }

    /**
     * Decorates spec-compliant $formattedError with debug entries according to $debug flags
     * (see GraphQL\Error\Debug for available flags)
     *
     * @param mixed[]   $formattedError
     * @param Throwable $e
     * @param bool|int  $debug
     *
     * @return mixed[]
     *
     * @throws Throwable
     */
    public static function addDebugEntries(array $formattedError, $e, $debug)
    {
        if (! $debug) {
            return $formattedError;
        }

        Utils::invariant(
            $e instanceof Exception || $e instanceof Throwable,
            'Expected exception, got %s',
            Utils::getVariableType($e)
        );

        $debug = (int) $debug;

        if ($debug & Debug::RETHROW_INTERNAL_EXCEPTIONS) {
            if (! $e instanceof Error) {
                throw $e;
            }

            if ($e->getPrevious()) {
                throw $e->getPrevious();
            }
        }

        $isUnsafe = ! $e instanceof ClientAware || ! $e->isClientSafe();

        if (($debug & Debug::RETHROW_UNSAFE_EXCEPTIONS) && $isUnsafe) {
            if ($e->getPrevious()) {
                throw $e->getPrevious();
            }
        }

        if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isUnsafe) {
            // Displaying debugMessage as a first entry:
            $formattedError = ['debugMessage' => $e->getMessage()] + $formattedError;
        }

        if ($debug & Debug::INCLUDE_TRACE) {
            if ($e instanceof ErrorException || $e instanceof \Error) {
                $formattedError += [
                    'file' => $e->getFile(),
                    'line' => $e->getLine(),
                ];
            }

            $isTrivial = $e instanceof Error && ! $e->getPrevious();

            if (! $isTrivial) {
                $debugging               = $e->getPrevious() ?: $e;
                $formattedError['trace'] = static::toSafeTrace($debugging);
            }
        }

        return $formattedError;
    }

    /**
     * Prepares final error formatter taking in account $debug flags.
     * If initial formatter is not set, FormattedError::createFromException is used
     *
     * @param  bool|int $debug
     *
     * @return callable|callable
     */
    public static function prepareFormatter(?callable $formatter = null, $debug)
    {
        $formatter = $formatter ?: static function ($e) {
            return FormattedError::createFromException($e);
        };
        if ($debug) {
            $formatter = static function ($e) use ($formatter, $debug) {
                return FormattedError::addDebugEntries($formatter($e), $e, $debug);
            };
        }

        return $formatter;
    }

    /**
     * Returns error trace as serializable array
     *
     * @param Throwable $error
     *
     * @return mixed[]
     *
     * @api
     */
    public static function toSafeTrace($error)
    {
        $trace = $error->getTrace();

        if (isset($trace[0]['function']) && isset($trace[0]['class']) &&
            // Remove invariant entries as they don't provide much value:
            ($trace[0]['class'] . '::' . $trace[0]['function'] === 'GraphQL\Utils\Utils::invariant')) {
            array_shift($trace);
        } elseif (! isset($trace[0]['file'])) {
            // Remove root call as it's likely error handler trace:
            array_shift($trace);
        }

        return array_map(
            static function ($err) {
                $safeErr = array_intersect_key($err, ['file' => true, 'line' => true]);

                if (isset($err['function'])) {
                    $func    = $err['function'];
                    $args    = ! empty($err['args']) ? array_map([self::class, 'printVar'], $err['args']) : [];
                    $funcStr = $func . '(' . implode(', ', $args) . ')';

                    if (isset($err['class'])) {
                        $safeErr['call'] = $err['class'] . '::' . $funcStr;
                    } else {
                        $safeErr['function'] = $funcStr;
                    }
                }

                return $safeErr;
            },
            $trace
        );
    }

    /**
     * @param mixed $var
     *
     * @return string
     */
    public static function printVar($var)
    {
        if ($var instanceof Type) {
            // FIXME: Replace with schema printer call
            if ($var instanceof WrappingType) {
                $var = $var->getWrappedType(true);
            }

            return 'GraphQLType: ' . $var->name;
        }

        if (is_object($var)) {
            return 'instance of ' . get_class($var) . ($var instanceof Countable ? '(' . count($var) . ')' : '');
        }
        if (is_array($var)) {
            return 'array(' . count($var) . ')';
        }
        if ($var === '') {
            return '(empty string)';
        }
        if (is_string($var)) {
            return "'" . addcslashes($var, "'") . "'";
        }
        if (is_bool($var)) {
            return $var ? 'true' : 'false';
        }
        if (is_scalar($var)) {
            return $var;
        }
        if ($var === null) {
            return 'null';
        }

        return gettype($var);
    }

    /**
     * @deprecated as of v0.8.0
     *
     * @param string           $error
     * @param SourceLocation[] $locations
     *
     * @return mixed[]
     */
    public static function create($error, array $locations = [])
    {
        $formatted = ['message' => $error];

        if (! empty($locations)) {
            $formatted['locations'] = array_map(
                static function ($loc) {
                    return $loc->toArray();
                },
                $locations
            );
        }

        return $formatted;
    }

    /**
     * @deprecated as of v0.10.0, use general purpose method createFromException() instead
     *
     * @return mixed[]
     */
    public static function createFromPHPError(ErrorException $e)
    {
        return [
            'message'  => $e->getMessage(),
            'severity' => $e->getSeverity(),
            'trace'    => self::toSafeTrace($e),
        ];
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Server;

use function array_change_key_case;
use function is_string;
use function json_decode;
use function json_last_error;
use const CASE_LOWER;

/**
 * Structure representing parsed HTTP parameters for GraphQL operation
 */
class OperationParams
{
    /**
     * Id of the query (when using persistent queries).
     *
     * Valid aliases (case-insensitive):
     * - id
     * - queryId
     * - documentId
     *
     * @api
     * @var string
     */
    public $queryId;

    /**
     * @api
     * @var string
     */
    public $query;

    /**
     * @api
     * @var string
     */
    public $operation;

    /**
     * @api
     * @var mixed[]|null
     */
    public $variables;

    /**
     * @api
     * @var mixed[]|null
     */
    public $extensions;

    /** @var mixed[] */
    private $originalInput;

    /** @var bool */
    private $readOnly;

    /**
     * Creates an instance from given array
     *
     * @param mixed[] $params
     *
     * @api
     */
    public static function create(array $params, bool $readonly = false) : OperationParams
    {
        $instance = new static();

        $params                  = array_change_key_case($params, CASE_LOWER);
        $instance->originalInput = $params;

        $params += [
            'query' => null,
            'queryid' => null,
            'documentid' => null, // alias to queryid
            'id' => null, // alias to queryid
            'operationname' => null,
            'variables' => null,
            'extensions' => null,
        ];

        if ($params['variables'] === '') {
            $params['variables'] = null;
        }

        // Some parameters could be provided as serialized JSON.
        foreach (['extensions', 'variables'] as $param) {
            if (! is_string($params[$param])) {
                continue;
            }

            $tmp = json_decode($params[$param], true);
            if (json_last_error()) {
                continue;
            }

            $params[$param] = $tmp;
        }

        $instance->query      = $params['query'];
        $instance->queryId    = $params['queryid'] ?: $params['documentid'] ?: $params['id'];
        $instance->operation  = $params['operationname'];
        $instance->variables  = $params['variables'];
        $instance->extensions = $params['extensions'];
        $instance->readOnly   = $readonly;

        // Apollo server/client compatibility: look for the queryid in extensions
        if (isset($instance->extensions['persistedQuery']['sha256Hash']) && empty($instance->query) && empty($instance->queryId)) {
            $instance->queryId = $instance->extensions['persistedQuery']['sha256Hash'];
        }

        return $instance;
    }

    /**
     * @param string $key
     *
     * @return mixed
     *
     * @api
     */
    public function getOriginalInput($key)
    {
        return $this->originalInput[$key] ?? null;
    }

    /**
     * Indicates that operation is executed in read-only context
     * (e.g. via HTTP GET request)
     *
     * @return bool
     *
     * @api
     */
    public function isReadOnly()
    {
        return $this->readOnly;
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Type\Definition;

use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Utils\Utils;
use function is_array;
use function is_string;
use function sprintf;

class FieldArgument
{
    /** @var string */
    public $name;

    /** @var mixed */
    public $defaultValue;

    /** @var string|null */
    public $description;

    /** @var InputValueDefinitionNode|null */
    public $astNode;

    /** @var mixed[] */
    public $config;

    /** @var InputType */
    private $type;

    /** @var bool */
    private $defaultValueExists = false;

    /**
     * @param mixed[] $def
     */
    public function __construct(array $def)
    {
        foreach ($def as $key => $value) {
            switch ($key) {
                case 'type':
                    $this->type = $value;
                    break;
                case 'name':
                    $this->name = $value;
                    break;
                case 'defaultValue':
                    $this->defaultValue       = $value;
                    $this->defaultValueExists = true;
                    break;
                case 'description':
                    $this->description = $value;
                    break;
                case 'astNode':
                    $this->astNode = $value;
                    break;
            }
        }
        $this->config = $def;
    }

    /**
     * @param mixed[] $config
     *
     * @return FieldArgument[]
     */
    public static function createMap(array $config)
    {
        $map = [];
        foreach ($config as $name => $argConfig) {
            if (! is_array($argConfig)) {
                $argConfig = ['type' => $argConfig];
            }
            $map[] = new self($argConfig + ['name' => $name]);
        }

        return $map;
    }

    /**
     * @return InputType
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @return bool
     */
    public function defaultValueExists()
    {
        return $this->defaultValueExists;
    }

    public function assertValid(FieldDefinition $parentField, Type $parentType)
    {
        try {
            Utils::assertValidName($this->name);
        } catch (InvariantViolation $e) {
            throw new InvariantViolation(
                sprintf('%s.%s(%s:) %s', $parentType->name, $parentField->name, $this->name, $e->getMessage())
            );
        }
        $type = $this->type;
        if ($type instanceof WrappingType) {
            $type = $type->getWrappedType(true);
        }
        Utils::invariant(
            $type instanceof InputType,
            sprintf(
                '%s.%s(%s): argument type must be Input Type but got: %s',
                $parentType->name,
                $parentField->name,
                $this->name,
                Utils::printSafe($this->type)
            )
        );
        Utils::invariant(
            $this->description === null || is_string($this->description),
            sprintf(
                '%s.%s(%s): argument description type must be string but got: %s',
                $parentType->name,
                $parentField->name,
                $this->name,
                Utils::printSafe($this->description)
            )
        );
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Type\Definition;

use Exception;
use GraphQL\Language\AST\Node;
use GraphQL\Utils\AST;
use GraphQL\Utils\Utils;
use function call_user_func;
use function is_callable;
use function sprintf;

class CustomScalarType extends ScalarType
{
    /**
     * @param mixed $value
     *
     * @return mixed
     */
    public function serialize($value)
    {
        return call_user_func($this->config['serialize'], $value);
    }

    /**
     * @param mixed $value
     *
     * @return mixed
     */
    public function parseValue($value)
    {
        if (isset($this->config['parseValue'])) {
            return call_user_func($this->config['parseValue'], $value);
        }

        return $value;
    }

    /**
     * @param Node         $valueNode
     * @param mixed[]|null $variables
     *
     * @return mixed
     *
     * @throws Exception
     */
    public function parseLiteral(/* GraphQL\Language\AST\ValueNode */
        $valueNode,
        ?array $variables = null
    ) {
        if (isset($this->config['parseLiteral'])) {
            return call_user_func($this->config['parseLiteral'], $valueNode, $variables);
        }

        return AST::valueFromASTUntyped($valueNode, $variables);
    }

    public function assertValid()
    {
        parent::assertValid();

        Utils::invariant(
            isset($this->config['serialize']) && is_callable($this->config['serialize']),
            sprintf('%s must provide "serialize" function. If this custom Scalar ', $this->name) .
            'is also used as an input type, ensure "parseValue" and "parseLiteral" ' .
            'functions are also provided.'
        );
        if (! isset($this->config['parseValue']) && ! isset($this->config['parseLiteral'])) {
            return;
        }

        Utils::invariant(
            isset($this->config['parseValue']) && isset($this->config['parseLiteral']) &&
            is_callable($this->config['parseValue']) && is_callable($this->config['parseLiteral']),
            sprintf('%s must provide both "parseValue" and "parseLiteral" functions.', $this->name)
        );
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Utils;

use function array_pop;
use function array_shift;
use function count;
use function implode;
use function mb_strlen;
use function mb_substr;
use function preg_split;
use function trim;

class BlockString
{
    /**
     * Produces the value of a block string from its parsed raw value, similar to
     * Coffeescript's block string, Python's docstring trim or Ruby's strip_heredoc.
     *
     * This implements the GraphQL spec's BlockStringValue() static algorithm.
     */
    public static function value($rawString)
    {
        // Expand a block string's raw value into independent lines.
        $lines = preg_split("/\\r\\n|[\\n\\r]/", $rawString);

        // Remove common indentation from all lines but first.
        $commonIndent = null;
        $linesLength  = count($lines);

        for ($i = 1; $i < $linesLength; $i++) {
            $line   = $lines[$i];
            $indent = self::leadingWhitespace($line);

            if ($indent >= mb_strlen($line) ||
                ($commonIndent !== null && $indent >= $commonIndent)
            ) {
                continue;
            }

            $commonIndent = $indent;
            if ($commonIndent === 0) {
                break;
            }
        }

        if ($commonIndent) {
            for ($i = 1; $i < $linesLength; $i++) {
                $line      = $lines[$i];
                $lines[$i] = mb_substr($line, $commonIndent);
            }
        }

        // Remove leading and trailing blank lines.
        while (count($lines) > 0 && trim($lines[0], " \t") === '') {
            array_shift($lines);
        }
        while (count($lines) > 0 && trim($lines[count($lines) - 1], " \t") === '') {
            array_pop($lines);
        }

        // Return a string of the lines joined with U+000A.
        return implode("\n", $lines);
    }

    private static function leadingWhitespace($str)
    {
        $i = 0;
        while ($i < mb_strlen($str) && ($str[$i] === ' ' || $str[$i] === '\t')) {
            $i++;
        }

        return $i;
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Executor;

use GraphQL\Error\Error;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Utils\AST;
use GraphQL\Utils\TypeInfo;
use GraphQL\Utils\Utils;
use GraphQL\Utils\Value;
use stdClass;
use Throwable;
use function array_key_exists;
use function array_map;
use function sprintf;

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 VariableDefinitionNode[] $varDefNodes
     * @param mixed[]                  $inputs
     *
     * @return mixed[]
     */
    public static function getVariableValues(Schema $schema, $varDefNodes, array $inputs)
    {
        $errors        = [];
        $coercedValues = [];
        foreach ($varDefNodes as $varDefNode) {
            $varName = $varDefNode->variable->name->value;
            /** @var InputType|Type $varType */
            $varType = TypeInfo::typeFromAST($schema, $varDefNode->type);

            if (Type::isInputType($varType)) {
                if (array_key_exists($varName, $inputs)) {
                    $value   = $inputs[$varName];
                    $coerced = Value::coerceValue($value, $varType, $varDefNode);
                    /** @var Error[] $coercionErrors */
                    $coercionErrors = $coerced['errors'];
                    if (empty($coercionErrors)) {
                        $coercedValues[$varName] = $coerced['value'];
                    } else {
                        $messagePrelude = sprintf(
                            'Variable "$%s" got invalid value %s; ',
                            $varName,
                            Utils::printSafeJson($value)
                        );

                        foreach ($coercionErrors as $error) {
                            $errors[] = new Error(
                                $messagePrelude . $error->getMessage(),
                                $error->getNodes(),
                                $error->getSource(),
                                $error->getPositions(),
                                $error->getPath(),
                                $error,
                                $error->getExtensions()
                            );
                        }
                    }
                } else {
                    if ($varType instanceof NonNull) {
                        $errors[] = new Error(
                            sprintf(
                                'Variable "$%s" of required type "%s" was not provided.',
                                $varName,
                                $varType
                            ),
                            [$varDefNode]
                        );
                    } elseif ($varDefNode->defaultValue) {
                        $coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType);
                    }
                }
            } else {
                $errors[] = new Error(
                    sprintf(
                        'Variable "$%s" expected value of type "%s" which cannot be used as an input type.',
                        $varName,
                        Printer::doPrint($varDefNode->type)
                    ),
                    [$varDefNode->type]
                );
            }
        }

        if (! empty($errors)) {
            return [$errors, null];
        }

        return [null, $coercedValues];
    }

    /**
     * Prepares an object map of argument values given a directive definition
     * and a AST node which may contain directives. Optionally also accepts a map
     * of variable values.
     *
     * If the directive does not exist on the node, returns undefined.
     *
     * @param FragmentSpreadNode|FieldNode|InlineFragmentNode|EnumValueDefinitionNode|FieldDefinitionNode $node
     * @param mixed[]|null                                                                                $variableValues
     *
     * @return mixed[]|null
     */
    public static function getDirectiveValues(Directive $directiveDef, $node, $variableValues = null)
    {
        if (isset($node->directives) && $node->directives instanceof NodeList) {
            $directiveNode = Utils::find(
                $node->directives,
                static function (DirectiveNode $directive) use ($directiveDef) {
                    return $directive->name->value === $directiveDef->name;
                }
            );

            if ($directiveNode !== null) {
                return self::getArgumentValues($directiveDef, $directiveNode, $variableValues);
            }
        }

        return null;
    }

    /**
     * Prepares an object map of argument values given a list of argument
     * definitions and list of argument AST nodes.
     *
     * @param FieldDefinition|Directive $def
     * @param FieldNode|DirectiveNode   $node
     * @param mixed[]                   $variableValues
     *
     * @return mixed[]
     *
     * @throws Error
     */
    public static function getArgumentValues($def, $node, $variableValues = null)
    {
        if (empty($def->args)) {
            return [];
        }

        $argumentNodes = $node->arguments;
        if (empty($argumentNodes)) {
            return [];
        }

        $argumentValueMap = [];
        foreach ($argumentNodes as $argumentNode) {
            $argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
        }

        return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node);
    }

    /**
     * @param FieldDefinition|Directive $fieldDefinition
     * @param ArgumentNode[]            $argumentValueMap
     * @param mixed[]                   $variableValues
     * @param Node|null                 $referenceNode
     *
     * @return mixed[]
     *
     * @throws Error
     */
    public static function getArgumentValuesForMap($fieldDefinition, $argumentValueMap, $variableValues = null, $referenceNode = null)
    {
        $argumentDefinitions = $fieldDefinition->args;
        $coercedValues       = [];

        foreach ($argumentDefinitions as $argumentDefinition) {
            $name              = $argumentDefinition->name;
            $argType           = $argumentDefinition->getType();
            $argumentValueNode = $argumentValueMap[$name] ?? null;

            if (! $argumentValueNode) {
                if ($argumentDefinition->defaultValueExists()) {
                    $coercedValues[$name] = $argumentDefinition->defaultValue;
                } elseif ($argType instanceof NonNull) {
                    throw new Error(
                        'Argument "' . $name . '" of required type ' .
                        '"' . Utils::printSafe($argType) . '" was not provided.',
                        $referenceNode
                    );
                }
            } elseif ($argumentValueNode instanceof VariableNode) {
                $variableName = $argumentValueNode->name->value;

                if ($variableValues && array_key_exists($variableName, $variableValues)) {
                    // Note: this does not check that this variable value is correct.
                    // This assumes that this query has been validated and the variable
                    // usage here is of the correct type.
                    $coercedValues[$name] = $variableValues[$variableName];
                } elseif ($argumentDefinition->defaultValueExists()) {
                    $coercedValues[$name] = $argumentDefinition->defaultValue;
                } elseif ($argType instanceof NonNull) {
                    throw new Error(
                        'Argument "' . $name . '" of required type "' . Utils::printSafe($argType) . '" was ' .
                        'provided the variable "$' . $variableName . '" which was not provided ' .
                        'a runtime value.',
                        [$argumentValueNode]
                    );
                }
            } else {
                $valueNode    = $argumentValueNode;
                $coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
                if (Utils::isInvalid($coercedValue)) {
                    // Note: ValuesOfCorrectType validation should catch this before
                    // execution. This is a runtime check to ensure execution does not
                    // continue with an invalid argument value.
                    throw new Error(
                        'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.',
                        [$argumentValueNode]
                    );
                }
                $coercedValues[$name] = $coercedValue;
            }
        }

        return $coercedValues;
    }

    /**
     * @deprecated as of 8.0 (Moved to \GraphQL\Utils\AST::valueFromAST)
     *
     * @param ValueNode    $valueNode
     * @param mixed[]|null $variables
     *
     * @return mixed[]|stdClass|null
     */
    public static function valueFromAST($valueNode, InputType $type, ?array $variables = null)
    {
        return AST::valueFromAST($valueNode, $type, $variables);
    }

    /**
     * @deprecated as of 0.12 (Use coerceValue() directly for richer information)
     *
     * @param mixed[] $value
     *
     * @return string[]
     */
    public static function isValidPHPValue($value, InputType $type)
    {
        $errors = Value::coerceValue($value, $type)['errors'];

        return $errors
            ? array_map(
                static function (Throwable $error) {
                    return $error->getMessage();
                },
                $errors
            ) : [];
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Utils;

use GraphQL\Error\Error;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Schema;
use function array_map;
use function array_reduce;
use function sprintf;

/**
 * Build instance of `GraphQL\Type\Schema` out of type language definition (string or parsed AST)
 * See [section in docs](type-system/type-language.md) for details.
 */
class BuildSchema
{
    /** @var DocumentNode */
    private $ast;

    /** @var Node[] */
    private $nodeMap;

    /** @var callable|null */
    private $typeConfigDecorator;

    /** @var bool[] */
    private $options;

    /**
     * @param bool[] $options
     */
    public function __construct(DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [])
    {
        $this->ast                 = $ast;
        $this->typeConfigDecorator = $typeConfigDecorator;
        $this->options             = $options;
    }

    /**
     * A helper function to build a GraphQLSchema directly from a source
     * document.
     *
     * @param DocumentNode|Source|string $source
     * @param bool[]                     $options
     *
     * @return Schema
     *
     * @api
     */
    public static function build($source, ?callable $typeConfigDecorator = null, array $options = [])
    {
        $doc = $source instanceof DocumentNode ? $source : Parser::parse($source);

        return self::buildAST($doc, $typeConfigDecorator, $options);
    }

    /**
     * This takes the ast of a schema document produced by the parse function in
     * GraphQL\Language\Parser.
     *
     * If no schema definition is provided, then it will look for types named Query
     * and Mutation.
     *
     * Given that AST it constructs a GraphQL\Type\Schema. The resulting schema
     * has no resolve methods, so execution will use default resolvers.
     *
     * Accepts options as a third argument:
     *
     *    - commentDescriptions:
     *        Provide true to use preceding comments as the description.
     *
     * @param bool[] $options
     *
     * @return Schema
     *
     * @throws Error
     *
     * @api
     */
    public static function buildAST(DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [])
    {
        $builder = new self($ast, $typeConfigDecorator, $options);

        return $builder->buildSchema();
    }

    public function buildSchema()
    {
        /** @var SchemaDefinitionNode $schemaDef */
        $schemaDef     = null;
        $typeDefs      = [];
        $this->nodeMap = [];
        $directiveDefs = [];
        foreach ($this->ast->definitions as $d) {
            switch ($d->kind) {
                case NodeKind::SCHEMA_DEFINITION:
                    if ($schemaDef) {
                        throw new Error('Must provide only one schema definition.');
                    }
                    $schemaDef = $d;
                    break;
                case NodeKind::SCALAR_TYPE_DEFINITION:
                case NodeKind::OBJECT_TYPE_DEFINITION:
                case NodeKind::INTERFACE_TYPE_DEFINITION:
                case NodeKind::ENUM_TYPE_DEFINITION:
                case NodeKind::UNION_TYPE_DEFINITION:
                case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
                    $typeName = $d->name->value;
                    if (! empty($this->nodeMap[$typeName])) {
                        throw new Error(sprintf('Type "%s" was defined more than once.', $typeName));
                    }
                    $typeDefs[]               = $d;
                    $this->nodeMap[$typeName] = $d;
                    break;
                case NodeKind::DIRECTIVE_DEFINITION:
                    $directiveDefs[] = $d;
                    break;
            }
        }

        $operationTypes = $schemaDef
            ? $this->getOperationTypes($schemaDef)
            : [
                'query'        => isset($this->nodeMap['Query']) ? 'Query' : null,
                'mutation'     => isset($this->nodeMap['Mutation']) ? 'Mutation' : null,
                'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null,
            ];

        $DefinitionBuilder = new ASTDefinitionBuilder(
            $this->nodeMap,
            $this->options,
            static function ($typeName) {
                throw new Error('Type "' . $typeName . '" not found in document.');
            },
            $this->typeConfigDecorator
        );

        $directives = array_map(
            static function ($def) use ($DefinitionBuilder) {
                return $DefinitionBuilder->buildDirective($def);
            },
            $directiveDefs
        );

        // If specified directives were not explicitly declared, add them.
        $skip = array_reduce(
            $directives,
            static function ($hasSkip, $directive) {
                return $hasSkip || $directive->name === 'skip';
            }
        );
        if (! $skip) {
            $directives[] = Directive::skipDirective();
        }

        $include = array_reduce(
            $directives,
            static function ($hasInclude, $directive) {
                return $hasInclude || $directive->name === 'include';
            }
        );
        if (! $include) {
            $directives[] = Directive::includeDirective();
        }

        $deprecated = array_reduce(
            $directives,
            static function ($hasDeprecated, $directive) {
                return $hasDeprecated || $directive->name === 'deprecated';
            }
        );
        if (! $deprecated) {
            $directives[] = Directive::deprecatedDirective();
        }

        // Note: While this could make early assertions to get the correctly
        // typed values below, that would throw immediately while type system
        // validation with validateSchema() will produce more actionable results.

        return new Schema([
            'query'        => isset($operationTypes['query'])
                ? $DefinitionBuilder->buildType($operationTypes['query'])
                : null,
            'mutation'     => isset($operationTypes['mutation'])
                ? $DefinitionBuilder->buildType($operationTypes['mutation'])
                : null,
            'subscription' => isset($operationTypes['subscription'])
                ? $DefinitionBuilder->buildType($operationTypes['subscription'])
                : null,
            'typeLoader'   => static function ($name) use ($DefinitionBuilder) {
                return $DefinitionBuilder->buildType($name);
            },
            'directives'   => $directives,
            'astNode'      => $schemaDef,
            'types'        => function () use ($DefinitionBuilder) {
                $types = [];
                foreach ($this->nodeMap as $name => $def) {
                    $types[] = $DefinitionBuilder->buildType($def->name->value);
                }

                return $types;
            },
        ]);
    }

    /**
     * @param SchemaDefinitionNode $schemaDef
     *
     * @return string[]
     *
     * @throws Error
     */
    private function getOperationTypes($schemaDef)
    {
        $opTypes = [];

        foreach ($schemaDef->operationTypes as $operationType) {
            $typeName  = $operationType->type->name->value;
            $operation = $operationType->operation;

            if (isset($opTypes[$operation])) {
                throw new Error(sprintf('Must provide only one %s type in schema.', $operation));
            }

            if (! isset($this->nodeMap[$typeName])) {
                throw new Error(sprintf('Specified %s type "%s" not found in document.', $operation, $typeName));
            }

            $opTypes[$operation] = $typeName;
        }

        return $opTypes;
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Utils;

use GraphQL\Error\Error;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\AST\SchemaTypeExtensionNode;
use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\AST\TypeExtensionNode;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\EnumValueDefinition;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Type\Schema;
use GraphQL\Validator\DocumentValidator;
use function array_keys;
use function array_map;
use function array_merge;
use function array_values;
use function count;

class SchemaExtender
{
    const SCHEMA_EXTENSION = 'SchemaExtension';

    /** @var Type[] */
    protected static $extendTypeCache;

    /** @var mixed[] */
    protected static $typeExtensionsMap;

    /** @var ASTDefinitionBuilder */
    protected static $astBuilder;

    /**
     * @return TypeExtensionNode[]|null
     */
    protected static function getExtensionASTNodes(NamedType $type) : ?array
    {
        if (! $type instanceof Type) {
            return null;
        }

        $name = $type->name;
        if ($type->extensionASTNodes !== null) {
            if (isset(static::$typeExtensionsMap[$name])) {
                return array_merge($type->extensionASTNodes, static::$typeExtensionsMap[$name]);
            }

            return $type->extensionASTNodes;
        }

        return static::$typeExtensionsMap[$name] ?? null;
    }

    /**
     * @throws Error
     */
    protected static function checkExtensionNode(Type $type, Node $node) : void
    {
        switch ($node->kind) {
            case NodeKind::OBJECT_TYPE_EXTENSION:
                if (! ($type instanceof ObjectType)) {
                    throw new Error(
                        'Cannot extend non-object type "' . $type->name . '".',
                        [$node]
                    );
                }
                break;
            case NodeKind::INTERFACE_TYPE_EXTENSION:
                if (! ($type instanceof InterfaceType)) {
                    throw new Error(
                        'Cannot extend non-interface type "' . $type->name . '".',
                        [$node]
                    );
                }
                break;
            case NodeKind::ENUM_TYPE_EXTENSION:
                if (! ($type instanceof EnumType)) {
                    throw new Error(
                        'Cannot extend non-enum type "' . $type->name . '".',
                        [$node]
                    );
                }
                break;
            case NodeKind::UNION_TYPE_EXTENSION:
                if (! ($type instanceof UnionType)) {
                    throw new Error(
                        'Cannot extend non-union type "' . $type->name . '".',
                        [$node]
                    );
                }
                break;
            case NodeKind::INPUT_OBJECT_TYPE_EXTENSION:
                if (! ($type instanceof InputObjectType)) {
                    throw new Error(
                        'Cannot extend non-input object type "' . $type->name . '".',
                        [$node]
                    );
                }
                break;
        }
    }

    protected static function extendCustomScalarType(CustomScalarType $type) : CustomScalarType
    {
        return new CustomScalarType([
            'name' => $type->name,
            'description' => $type->description,
            'astNode' => $type->astNode,
            'serialize' => $type->config['serialize'] ?? null,
            'parseValue' => $type->config['parseValue'] ?? null,
            'parseLiteral' => $type->config['parseLiteral'] ?? null,
            'extensionASTNodes' => static::getExtensionASTNodes($type),
        ]);
    }

    protected static function extendUnionType(UnionType $type) : UnionType
    {
        return new UnionType([
            'name' => $type->name,
            'description' => $type->description,
            'types' => static function () use ($type) {
                return static::extendPossibleTypes($type);
            },
            'astNode' => $type->astNode,
            'resolveType' => $type->config['resolveType'] ?? null,
            'extensionASTNodes' => static::getExtensionASTNodes($type),
        ]);
    }

    protected static function extendEnumType(EnumType $type) : EnumType
    {
        return new EnumType([
            'name' => $type->name,
            'description' => $type->description,
            'values' => static::extendValueMap($type),
            'astNode' => $type->astNode,
            'extensionASTNodes' => static::getExtensionASTNodes($type),
        ]);
    }

    protected static function extendInputObjectType(InputObjectType $type) : InputObjectType
    {
        return new InputObjectType([
            'name' => $type->name,
            'description' => $type->description,
            'fields' => static function () use ($type) {
                return static::extendInputFieldMap($type);
            },
            'astNode' => $type->astNode,
            'extensionASTNodes' => static::getExtensionASTNodes($type),
        ]);
    }

    /**
     * @return mixed[]
     */
    protected static function extendInputFieldMap(InputObjectType $type) : array
    {
        $newFieldMap = [];
        $oldFieldMap = $type->getFields();
        foreach ($oldFieldMap as $fieldName => $field) {
            $newFieldMap[$fieldName] = [
                'description' => $field->description,
                'type' => static::extendType($field->type),
                'astNode' => $field->astNode,
            ];

            if (! $field->defaultValueExists()) {
                continue;
            }

            $newFieldMap[$fieldName]['defaultValue'] = $field->defaultValue;
        }

        $extensions = static::$typeExtensionsMap[$type->name] ?? null;
        if ($extensions !== null) {
            foreach ($extensions as $extension) {
                foreach ($extension->fields as $field) {
                    $fieldName = $field->name->value;
                    if (isset($oldFieldMap[$fieldName])) {
                        throw new Error('Field "' . $type->name . '.' . $fieldName . '" already exists in the schema. It cannot also be defined in this type extension.', [$field]);
                    }

                    $newFieldMap[$fieldName] = static::$astBuilder->buildInputField($field);
                }
            }
        }

        return $newFieldMap;
    }

    /**
     * @return mixed[]
     */
    protected static function extendValueMap(EnumType $type) : array
    {
        $newValueMap = [];
        /** @var EnumValueDefinition[] $oldValueMap */
        $oldValueMap = [];
        foreach ($type->getValues() as $value) {
            $oldValueMap[$value->name] = $value;
        }

        foreach ($oldValueMap as $key => $value) {
            $newValueMap[$key] = [
                'name' => $value->name,
                'description' => $value->description,
                'value' => $value->value,
                'deprecationReason' => $value->deprecationReason,
                'astNode' => $value->astNode,
            ];
        }

        $extensions = static::$typeExtensionsMap[$type->name] ?? null;
        if ($extensions !== null) {
            foreach ($extensions as $extension) {
                foreach ($extension->values as $value) {
                    $valueName = $value->name->value;
                    if (isset($oldValueMap[$valueName])) {
                        throw new Error('Enum value "' . $type->name . '.' . $valueName . '" already exists in the schema. It cannot also be defined in this type extension.', [$value]);
                    }
                    $newValueMap[$valueName] = static::$astBuilder->buildEnumValue($value);
                }
            }
        }

        return $newValueMap;
    }

    /**
     * @return ObjectType[]
     */
    protected static function extendPossibleTypes(UnionType $type) : array
    {
        $possibleTypes = array_map(static function ($type) {
            return static::extendNamedType($type);
        }, $type->getTypes());

        $extensions = static::$typeExtensionsMap[$type->name] ?? null;
        if ($extensions !== null) {
            foreach ($extensions as $extension) {
                foreach ($extension->types as $namedType) {
                    $possibleTypes[] = static::$astBuilder->buildType($namedType);
                }
            }
        }

        return $possibleTypes;
    }

    /**
     * @return InterfaceType[]
     */
    protected static function extendImplementedInterfaces(ObjectType $type) : array
    {
        $interfaces = array_map(static function (InterfaceType $interfaceType) {
            return static::extendNamedType($interfaceType);
        }, $type->getInterfaces());

        $extensions = static::$typeExtensionsMap[$type->name] ?? null;
        if ($extensions !== null) {
            /** @var ObjectTypeExtensionNode $extension */
            foreach ($extensions as $extension) {
                foreach ($extension->interfaces as $namedType) {
                    $interfaces[] = static::$astBuilder->buildType($namedType);
                }
            }
        }

        return $interfaces;
    }

    protected static function extendType($typeDef)
    {
        if ($typeDef instanceof ListOfType) {
            return Type::listOf(static::extendType($typeDef->ofType));
        }

        if ($typeDef instanceof NonNull) {
            return Type::nonNull(static::extendType($typeDef->getWrappedType()));
        }

        return static::extendNamedType($typeDef);
    }

    /**
     * @param FieldArgument[] $args
     *
     * @return mixed[]
     */
    protected static function extendArgs(array $args) : array
    {
        return Utils::keyValMap(
            $args,
            static function (FieldArgument $arg) {
                return $arg->name;
            },
            static function (FieldArgument $arg) {
                $def = [
                    'type'        => static::extendType($arg->getType()),
                    'description' => $arg->description,
                    'astNode'     => $arg->astNode,
                ];

                if ($arg->defaultValueExists()) {
                    $def['defaultValue'] = $arg->defaultValue;
                }

                return $def;
            }
        );
    }

    /**
     * @param InterfaceType|ObjectType $type
     *
     * @return mixed[]
     *
     * @throws Error
     */
    protected static function extendFieldMap($type) : array
    {
        $newFieldMap = [];
        $oldFieldMap = $type->getFields();

        foreach (array_keys($oldFieldMap) as $fieldName) {
            $field = $oldFieldMap[$fieldName];

            $newFieldMap[$fieldName] = [
                'name' => $fieldName,
                'description' => $field->description,
                'deprecationReason' => $field->deprecationReason,
                'type' => static::extendType($field->getType()),
                'args' => static::extendArgs($field->args),
                'astNode' => $field->astNode,
                'resolve' => $field->resolveFn,
            ];
        }

        $extensions = static::$typeExtensionsMap[$type->name] ?? null;
        if ($extensions !== null) {
            foreach ($extensions as $extension) {
                foreach ($extension->fields as $field) {
                    $fieldName = $field->name->value;
                    if (isset($oldFieldMap[$fieldName])) {
                        throw new Error('Field "' . $type->name . '.' . $fieldName . '" already exists in the schema. It cannot also be defined in this type extension.', [$field]);
                    }

                    $newFieldMap[$fieldName] = static::$astBuilder->buildField($field);
                }
            }
        }

        return $newFieldMap;
    }

    protected static function extendObjectType(ObjectType $type) : ObjectType
    {
        return new ObjectType([
            'name' => $type->name,
            'description' => $type->description,
            'interfaces' => static function () use ($type) {
                return static::extendImplementedInterfaces($type);
            },
            'fields' => static function () use ($type) {
                return static::extendFieldMap($type);
            },
            'astNode' => $type->astNode,
            'extensionASTNodes' => static::getExtensionASTNodes($type),
            'isTypeOf' => $type->config['isTypeOf'] ?? null,
        ]);
    }

    protected static function extendInterfaceType(InterfaceType $type) : InterfaceType
    {
        return new InterfaceType([
            'name' => $type->name,
            'description' => $type->description,
            'fields' => static function () use ($type) {
                return static::extendFieldMap($type);
            },
            'astNode' => $type->astNode,
            'extensionASTNodes' => static::getExtensionASTNodes($type),
            'resolveType' => $type->config['resolveType'] ?? null,
        ]);
    }

    protected static function isSpecifiedScalarType(Type $type) : bool
    {
        return $type instanceof NamedType &&
            (
                $type->name === Type::STRING ||
                $type->name === Type::INT ||
                $type->name === Type::FLOAT ||
                $type->name === Type::BOOLEAN ||
                $type->name === Type::ID
            );
    }

    protected static function extendNamedType(Type $type)
    {
        if (Introspection::isIntrospectionType($type) || static::isSpecifiedScalarType($type)) {
            return $type;
        }

        $name = $type->name;
        if (! isset(static::$extendTypeCache[$name])) {
            if ($type instanceof CustomScalarType) {
                static::$extendTypeCache[$name] = static::extendCustomScalarType($type);
            } elseif ($type instanceof ObjectType) {
                static::$extendTypeCache[$name] = static::extendObjectType($type);
            } elseif ($type instanceof InterfaceType) {
                static::$extendTypeCache[$name] = static::extendInterfaceType($type);
            } elseif ($type instanceof UnionType) {
                static::$extendTypeCache[$name] = static::extendUnionType($type);
            } elseif ($type instanceof EnumType) {
                static::$extendTypeCache[$name] = static::extendEnumType($type);
            } elseif ($type instanceof InputObjectType) {
                static::$extendTypeCache[$name] = static::extendInputObjectType($type);
            }
        }

        return static::$extendTypeCache[$name];
    }

    /**
     * @return mixed|null
     */
    protected static function extendMaybeNamedType(?NamedType $type = null)
    {
        if ($type !== null) {
            return static::extendNamedType($type);
        }

        return null;
    }

    /**
     * @param DirectiveDefinitionNode[] $directiveDefinitions
     *
     * @return Directive[]
     */
    protected static function getMergedDirectives(Schema $schema, array $directiveDefinitions) : array
    {
        $existingDirectives = array_map(static function (Directive $directive) {
            return static::extendDirective($directive);
        }, $schema->getDirectives());

        Utils::invariant(count($existingDirectives) > 0, 'schema must have default directives');

        return array_merge(
            $existingDirectives,
            array_map(static function (DirectiveDefinitionNode $directive) {
                return static::$astBuilder->buildDirective($directive);
            }, $directiveDefinitions)
        );
    }

    protected static function extendDirective(Directive $directive) : Directive
    {
        return new Directive([
            'name' => $directive->name,
            'description' => $directive->description,
            'locations' => $directive->locations,
            'args' => static::extendArgs($directive->args),
            'astNode' => $directive->astNode,
        ]);
    }

    /**
     * @param mixed[]|null $options
     */
    public static function extend(Schema $schema, DocumentNode $documentAST, ?array $options = null) : Schema
    {
        if ($options === null || ! (isset($options['assumeValid']) || isset($options['assumeValidSDL']))) {
            DocumentValidator::assertValidSDLExtension($documentAST, $schema);
        }

        $typeDefinitionMap         = [];
        static::$typeExtensionsMap = [];
        $directiveDefinitions      = [];
        /** @var SchemaDefinitionNode|null $schemaDef */
        $schemaDef = null;
        /** @var SchemaTypeExtensionNode[] $schemaExtensions */
        $schemaExtensions = [];

        $definitionsCount = count($documentAST->definitions);
        for ($i = 0; $i < $definitionsCount; $i++) {

            /** @var Node $def */
            $def = $documentAST->definitions[$i];

            if ($def instanceof SchemaDefinitionNode) {
                $schemaDef = $def;
            } elseif ($def instanceof SchemaTypeExtensionNode) {
                $schemaExtensions[] = $def;
            } elseif ($def instanceof TypeDefinitionNode) {
                $typeName = isset($def->name) ? $def->name->value : null;

                try {
                    $type = $schema->getType($typeName);
                } catch (Error $error) {
                    $type = null;
                }

                if ($type) {
                    throw new Error('Type "' . $typeName . '" already exists in the schema. It cannot also be defined in this type definition.', [$def]);
                }
                $typeDefinitionMap[$typeName] = $def;
            } elseif ($def instanceof TypeExtensionNode) {
                $extendedTypeName = isset($def->name) ? $def->name->value : null;
                $existingType     = $schema->getType($extendedTypeName);
                if ($existingType === null) {
                    throw new Error('Cannot extend type "' . $extendedTypeName . '" because it does not exist in the existing schema.', [$def]);
                }

                static::checkExtensionNode($existingType, $def);

                $existingTypeExtensions                       = static::$typeExtensionsMap[$extendedTypeName] ?? null;
                static::$typeExtensionsMap[$extendedTypeName] = $existingTypeExtensions !== null ? array_merge($existingTypeExtensions, [$def]) : [$def];
            } elseif ($def instanceof DirectiveDefinitionNode) {
                $directiveName     = $def->name->value;
                $existingDirective = $schema->getDirective($directiveName);
                if ($existingDirective !== null) {
                    throw new Error('Directive "' . $directiveName . '" already exists in the schema. It cannot be redefined.', [$def]);
                }
                $directiveDefinitions[] = $def;
            }
        }

        if (count(static::$typeExtensionsMap) === 0 &&
            count($typeDefinitionMap) === 0 &&
            count($directiveDefinitions) === 0 &&
            count($schemaExtensions) === 0 &&
            $schemaDef === null
        ) {
            return $schema;
        }

        static::$astBuilder = new ASTDefinitionBuilder(
            $typeDefinitionMap,
            $options,
            static function (string $typeName) use ($schema) {
                /** @var NamedType $existingType */
                $existingType = $schema->getType($typeName);
                if ($existingType !== null) {
                    return static::extendNamedType($existingType);
                }

                throw new Error('Unknown type: "' . $typeName . '". Ensure that this type exists either in the original schema, or is added in a type definition.', [$typeName]);
            }
        );

        static::$extendTypeCache = [];

        $operationTypes = [
            'query' => static::extendMaybeNamedType($schema->getQueryType()),
            'mutation' => static::extendMaybeNamedType($schema->getMutationType()),
            'subscription' => static::extendMaybeNamedType($schema->getSubscriptionType()),
        ];

        if ($schemaDef) {
            foreach ($schemaDef->operationTypes as $operationType) {
                $operation = $operationType->operation;
                $type      = $operationType->type;

                if (isset($operationTypes[$operation])) {
                    throw new Error('Must provide only one ' . $operation . ' type in schema.');
                }

                $operationTypes[$operation] = static::$astBuilder->buildType($type);
            }
        }

        foreach ($schemaExtensions as $schemaExtension) {
            if (! $schemaExtension->operationTypes) {
                continue;
            }

            foreach ($schemaExtension->operationTypes as $operationType) {
                $operation = $operationType->operation;
                if (isset($operationTypes[$operation])) {
                    throw new Error('Must provide only one ' . $operation . ' type in schema.');
                }
                $operationTypes[$operation] = static::$astBuilder->buildType($operationType->type);
            }
        }

        $schemaExtensionASTNodes = count($schemaExtensions) > 0
            ? ($schema->extensionASTNodes ? array_merge($schema->extensionASTNodes, $schemaExtensions) : $schemaExtensions)
            : $schema->extensionASTNodes;

        $types = array_merge(
            array_map(static function ($type) {
                return static::extendType($type);
            }, array_values($schema->getTypeMap())),
            array_map(static function ($type) {
                return static::$astBuilder->buildType($type);
            }, array_values($typeDefinitionMap))
        );

        return new Schema([
            'query' => $operationTypes['query'],
            'mutation' => $operationTypes['mutation'],
            'subscription' => $operationTypes['subscription'],
            'types' => $types,
            'directives' => static::getMergedDirectives($schema, $directiveDefinitions),
            'astNode' => $schema->getAstNode(),
            'extensionASTNodes' => $schemaExtensionASTNodes,
        ]);
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Type\Definition;

use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
use GraphQL\Utils\Utils;
use function is_callable;
use function is_string;
use function sprintf;

class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NullableType, NamedType
{
    /** @var InterfaceTypeDefinitionNode|null */
    public $astNode;

    /** @var InterfaceTypeExtensionNode[] */
    public $extensionASTNodes;

    /** @var FieldDefinition[] */
    private $fields;

    /**
     * @param mixed[] $config
     */
    public function __construct(array $config)
    {
        if (! isset($config['name'])) {
            $config['name'] = $this->tryInferName();
        }

        Utils::invariant(is_string($config['name']), 'Must provide name.');

        $this->name              = $config['name'];
        $this->description       = $config['description'] ?? null;
        $this->astNode           = $config['astNode'] ?? null;
        $this->extensionASTNodes = $config['extensionASTNodes'] ?? null;
        $this->config            = $config;
    }

    /**
     * @param mixed $type
     *
     * @return self
     */
    public static function assertInterfaceType($type)
    {
        Utils::invariant(
            $type instanceof self,
            'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Interface type.'
        );

        return $type;
    }

    /**
     * @param string $name
     *
     * @return FieldDefinition
     */
    public function getField($name)
    {
        if ($this->fields === null) {
            $this->getFields();
        }
        Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name);

        return $this->fields[$name];
    }

    /**
     * @param string $name
     *
     * @return bool
     */
    public function hasField($name)
    {
        if ($this->fields === null) {
            $this->getFields();
        }

        return isset($this->fields[$name]);
    }

    /**
     * @return FieldDefinition[]
     */
    public function getFields()
    {
        if ($this->fields === null) {
            $fields       = $this->config['fields'] ?? [];
            $this->fields = FieldDefinition::defineFieldMap($this, $fields);
        }

        return $this->fields;
    }

    /**
     * Resolves concrete ObjectType for given object value
     *
     * @param object  $objectValue
     * @param mixed[] $context
     *
     * @return Type|null
     */
    public function resolveType($objectValue, $context, ResolveInfo $info)
    {
        if (isset($this->config['resolveType'])) {
            $fn = $this->config['resolveType'];

            return $fn($objectValue, $context, $info);
        }

        return null;
    }

    /**
     * @throws InvariantViolation
     */
    public function assertValid()
    {
        parent::assertValid();

        $resolveType = $this->config['resolveType'] ?? null;

        Utils::invariant(
            ! isset($resolveType) || is_callable($resolveType),
            sprintf(
                '%s must provide "resolveType" as a function, but got: %s',
                $this->name,
                Utils::printSafe($resolveType)
            )
        );
    }
}