diff --git a/src/Language/AST/DirectiveDefinitionNode.php b/src/Language/AST/DirectiveDefinitionNode.php index 1e80084..84b649b 100644 --- a/src/Language/AST/DirectiveDefinitionNode.php +++ b/src/Language/AST/DirectiveDefinitionNode.php @@ -22,4 +22,9 @@ class DirectiveDefinitionNode extends Node implements TypeSystemDefinitionNode * @var NameNode[] */ public $locations; + + /** + * @var StringValueNode|null + */ + public $description; } diff --git a/src/Language/AST/EnumTypeDefinitionNode.php b/src/Language/AST/EnumTypeDefinitionNode.php index 3d1113c..71ca508 100644 --- a/src/Language/AST/EnumTypeDefinitionNode.php +++ b/src/Language/AST/EnumTypeDefinitionNode.php @@ -24,7 +24,7 @@ class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode public $values; /** - * @var string + * @var StringValueNode|null */ public $description; } diff --git a/src/Language/AST/EnumValueDefinitionNode.php b/src/Language/AST/EnumValueDefinitionNode.php index 45e6b2d..dd1c535 100644 --- a/src/Language/AST/EnumValueDefinitionNode.php +++ b/src/Language/AST/EnumValueDefinitionNode.php @@ -19,7 +19,7 @@ class EnumValueDefinitionNode extends Node public $directives; /** - * @var string + * @var StringValueNode|null */ public $description; } diff --git a/src/Language/AST/FieldDefinitionNode.php b/src/Language/AST/FieldDefinitionNode.php index 97639d3..d081d7f 100644 --- a/src/Language/AST/FieldDefinitionNode.php +++ b/src/Language/AST/FieldDefinitionNode.php @@ -29,7 +29,7 @@ class FieldDefinitionNode extends Node public $directives; /** - * @var string + * @var StringValueNode|null */ public $description; } diff --git a/src/Language/AST/InputObjectTypeDefinitionNode.php b/src/Language/AST/InputObjectTypeDefinitionNode.php index 17e0d07..c56ceca 100644 --- a/src/Language/AST/InputObjectTypeDefinitionNode.php +++ b/src/Language/AST/InputObjectTypeDefinitionNode.php @@ -24,7 +24,7 @@ class InputObjectTypeDefinitionNode extends Node implements TypeDefinitionNode public $fields; /** - * @var string + * @var StringValueNode|null */ public $description; } diff --git a/src/Language/AST/InputValueDefinitionNode.php b/src/Language/AST/InputValueDefinitionNode.php index 7dc65c4..47a0603 100644 --- a/src/Language/AST/InputValueDefinitionNode.php +++ b/src/Language/AST/InputValueDefinitionNode.php @@ -29,7 +29,7 @@ class InputValueDefinitionNode extends Node public $directives; /** - * @var string + * @var StringValueNode|null */ public $description; } diff --git a/src/Language/AST/InterfaceTypeDefinitionNode.php b/src/Language/AST/InterfaceTypeDefinitionNode.php index 60d2fd5..ff9bc1f 100644 --- a/src/Language/AST/InterfaceTypeDefinitionNode.php +++ b/src/Language/AST/InterfaceTypeDefinitionNode.php @@ -24,7 +24,7 @@ class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode public $fields = []; /** - * @var string + * @var StringValueNode|null */ public $description; } diff --git a/src/Language/AST/ObjectTypeDefinitionNode.php b/src/Language/AST/ObjectTypeDefinitionNode.php index 82d77c4..addf20a 100644 --- a/src/Language/AST/ObjectTypeDefinitionNode.php +++ b/src/Language/AST/ObjectTypeDefinitionNode.php @@ -29,7 +29,7 @@ class ObjectTypeDefinitionNode extends Node implements TypeDefinitionNode public $fields; /** - * @var string + * @var StringValueNode|null */ public $description; } diff --git a/src/Language/AST/ScalarTypeDefinitionNode.php b/src/Language/AST/ScalarTypeDefinitionNode.php index 483fb89..058841a 100644 --- a/src/Language/AST/ScalarTypeDefinitionNode.php +++ b/src/Language/AST/ScalarTypeDefinitionNode.php @@ -19,7 +19,7 @@ class ScalarTypeDefinitionNode extends Node implements TypeDefinitionNode public $directives; /** - * @var string + * @var StringValueNode|null */ public $description; } diff --git a/src/Language/AST/UnionTypeDefinitionNode.php b/src/Language/AST/UnionTypeDefinitionNode.php index 7653b75..0eae11a 100644 --- a/src/Language/AST/UnionTypeDefinitionNode.php +++ b/src/Language/AST/UnionTypeDefinitionNode.php @@ -24,7 +24,7 @@ class UnionTypeDefinitionNode extends Node implements TypeDefinitionNode public $types = []; /** - * @var string + * @var StringValueNode|null */ public $description; } diff --git a/src/Language/Lexer.php b/src/Language/Lexer.php index c00e20a..5ea7992 100644 --- a/src/Language/Lexer.php +++ b/src/Language/Lexer.php @@ -92,13 +92,18 @@ class Lexer */ public function advance() { - $token = $this->lastToken = $this->token; + $this->lastToken = $this->token; + $token = $this->token = $this->lookahead(); + return $token; + } + public function lookahead() + { + $token = $this->token; if ($token->kind !== Token::EOF) { do { - $token = $token->next = $this->readToken($token); + $token = $token->next ?: ($token->next = $this->readToken($token)); } while ($token->kind === Token::COMMENT); - $this->token = $token; } return $token; } diff --git a/src/Language/Parser.php b/src/Language/Parser.php index 2a8b532..bfc89e8 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -340,7 +340,7 @@ class Parser case 'fragment': return $this->parseFragmentDefinition(); - // Note: the Type System IDL is an experimental non-spec addition. + // Note: The schema definition language is an experimental addition. case 'schema': case 'scalar': case 'type': @@ -354,6 +354,11 @@ class Parser } } + // Note: The schema definition language is an experimental addition. + if ($this->peekDescription()) { + return $this->parseTypeSystemDefinition(); + } + throw $this->unexpected(); } @@ -656,12 +661,7 @@ class Parser ]); case Token::STRING: case Token::BLOCK_STRING: - $this->lexer->advance(); - return new StringValueNode([ - 'value' => $token->value, - 'block' => $token->kind === Token::BLOCK_STRING, - 'loc' => $this->loc($token) - ]); + return $this->parseStringLiteral(); case Token::NAME: if ($token->value === 'true' || $token->value === 'false') { $this->lexer->advance(); @@ -692,6 +692,20 @@ class Parser throw $this->unexpected(); } + /** + * @return StringValueNode + */ + function parseStringLiteral() { + $token = $this->lexer->token; + $this->lexer->advance(); + + return new StringValueNode([ + 'value' => $token->value, + 'block' => $token->kind === Token::BLOCK_STRING, + 'loc' => $this->loc($token) + ]); + } + /** * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode * @throws SyntaxError @@ -852,8 +866,13 @@ class Parser */ function parseTypeSystemDefinition() { - if ($this->peek(Token::NAME)) { - switch ($this->lexer->token->value) { + // Many definitions begin with a description and require a lookahead. + $keywordToken = $this->peekDescription() + ? $this->lexer->lookahead() + : $this->lexer->token; + + if ($keywordToken->kind === Token::NAME) { + switch ($keywordToken->value) { case 'schema': return $this->parseSchemaDefinition(); case 'scalar': return $this->parseScalarTypeDefinition(); case 'type': return $this->parseObjectTypeDefinition(); @@ -869,6 +888,22 @@ class Parser throw $this->unexpected(); } + /** + * @return bool + */ + function peekDescription() { + return $this->peek(Token::STRING) || $this->peek(Token::BLOCK_STRING); + } + + /** + * @return StringValueNode|null + */ + function parseDescription() { + if ($this->peekDescription()) { + return $this->parseStringLiteral(); + } + } + /** * @return SchemaDefinitionNode * @throws SyntaxError @@ -916,12 +951,11 @@ class Parser function parseScalarTypeDefinition() { $start = $this->lexer->token; + $description = $this->parseDescription(); $this->expectKeyword('scalar'); $name = $this->parseName(); $directives = $this->parseDirectives(); - $description = $this->getDescriptionFromAdjacentCommentTokens($start); - return new ScalarTypeDefinitionNode([ 'name' => $name, 'directives' => $directives, @@ -937,6 +971,7 @@ class Parser function parseObjectTypeDefinition() { $start = $this->lexer->token; + $description = $this->parseDescription(); $this->expectKeyword('type'); $name = $this->parseName(); $interfaces = $this->parseImplementsInterfaces(); @@ -948,8 +983,6 @@ class Parser Token::BRACE_R ); - $description = $this->getDescriptionFromAdjacentCommentTokens($start); - return new ObjectTypeDefinitionNode([ 'name' => $name, 'interfaces' => $interfaces, @@ -982,14 +1015,13 @@ class Parser function parseFieldDefinition() { $start = $this->lexer->token; + $description = $this->parseDescription(); $name = $this->parseName(); $args = $this->parseArgumentDefs(); $this->expect(Token::COLON); $type = $this->parseTypeReference(); $directives = $this->parseDirectives(); - $description = $this->getDescriptionFromAdjacentCommentTokens($start); - return new FieldDefinitionNode([ 'name' => $name, 'arguments' => $args, @@ -1018,6 +1050,7 @@ class Parser function parseInputValueDef() { $start = $this->lexer->token; + $description = $this->parseDescription(); $name = $this->parseName(); $this->expect(Token::COLON); $type = $this->parseTypeReference(); @@ -1026,7 +1059,6 @@ class Parser $defaultValue = $this->parseConstValue(); } $directives = $this->parseDirectives(); - $description = $this->getDescriptionFromAdjacentCommentTokens($start); return new InputValueDefinitionNode([ 'name' => $name, 'type' => $type, @@ -1044,6 +1076,7 @@ class Parser function parseInterfaceTypeDefinition() { $start = $this->lexer->token; + $description = $this->parseDescription(); $this->expectKeyword('interface'); $name = $this->parseName(); $directives = $this->parseDirectives(); @@ -1053,8 +1086,6 @@ class Parser Token::BRACE_R ); - $description = $this->getDescriptionFromAdjacentCommentTokens($start); - return new InterfaceTypeDefinitionNode([ 'name' => $name, 'directives' => $directives, @@ -1071,14 +1102,13 @@ class Parser function parseUnionTypeDefinition() { $start = $this->lexer->token; + $description = $this->parseDescription(); $this->expectKeyword('union'); $name = $this->parseName(); $directives = $this->parseDirectives(); $this->expect(Token::EQUALS); $types = $this->parseUnionMembers(); - $description = $this->getDescriptionFromAdjacentCommentTokens($start); - return new UnionTypeDefinitionNode([ 'name' => $name, 'directives' => $directives, @@ -1114,6 +1144,7 @@ class Parser function parseEnumTypeDefinition() { $start = $this->lexer->token; + $description = $this->parseDescription(); $this->expectKeyword('enum'); $name = $this->parseName(); $directives = $this->parseDirectives(); @@ -1123,8 +1154,6 @@ class Parser Token::BRACE_R ); - $description = $this->getDescriptionFromAdjacentCommentTokens($start); - return new EnumTypeDefinitionNode([ 'name' => $name, 'directives' => $directives, @@ -1140,11 +1169,10 @@ class Parser function parseEnumValueDefinition() { $start = $this->lexer->token; + $description = $this->parseDescription(); $name = $this->parseName(); $directives = $this->parseDirectives(); - $description = $this->getDescriptionFromAdjacentCommentTokens($start); - return new EnumValueDefinitionNode([ 'name' => $name, 'directives' => $directives, @@ -1160,6 +1188,7 @@ class Parser function parseInputObjectTypeDefinition() { $start = $this->lexer->token; + $description = $this->parseDescription(); $this->expectKeyword('input'); $name = $this->parseName(); $directives = $this->parseDirectives(); @@ -1169,8 +1198,6 @@ class Parser Token::BRACE_R ); - $description = $this->getDescriptionFromAdjacentCommentTokens($start); - return new InputObjectTypeDefinitionNode([ 'name' => $name, 'directives' => $directives, @@ -1206,6 +1233,7 @@ class Parser function parseDirectiveDefinition() { $start = $this->lexer->token; + $description = $this->parseDescription(); $this->expectKeyword('directive'); $this->expect(Token::AT); $name = $this->parseName(); @@ -1217,7 +1245,8 @@ class Parser 'name' => $name, 'arguments' => $args, 'locations' => $locations, - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), + 'description' => $description ]); } @@ -1234,28 +1263,4 @@ class Parser } while ($this->skip(Token::PIPE)); return $locations; } - - /** - * @param Token $nameToken - * @return null|string - */ - private function getDescriptionFromAdjacentCommentTokens(Token $nameToken) - { - $description = null; - - $currentToken = $nameToken; - $previousToken = $currentToken->prev; - - while ($previousToken->kind == Token::COMMENT - && ($previousToken->line + 1) == $currentToken->line - ) { - $description = $previousToken->value . $description; - - // walk the tokens backwards until no longer adjacent comments - $currentToken = $previousToken; - $previousToken = $currentToken->prev; - } - - return $description; - } } diff --git a/src/Language/Printer.php b/src/Language/Printer.php index 247cf49..edf5510 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -138,9 +138,9 @@ class Printer NodeKind::FLOAT => function(FloatValueNode $node) { return $node->value; }, - NodeKind::STRING => function(StringValueNode $node) { + NodeKind::STRING => function(StringValueNode $node, $key) { if ($node->block) { - return $this->printBlockString($node->value); + return $this->printBlockString($node->value, $key === 'description'); } return json_encode($node->value); }, @@ -192,74 +192,101 @@ class Printer }, NodeKind::SCALAR_TYPE_DEFINITION => function(ScalarTypeDefinitionNode $def) { - return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' '); + return $this->join([ + $def->description, + $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' ') + ], "\n"); }, NodeKind::OBJECT_TYPE_DEFINITION => function(ObjectTypeDefinitionNode $def) { return $this->join([ - 'type', - $def->name, - $this->wrap('implements ', $this->join($def->interfaces, ', ')), - $this->join($def->directives, ' '), - $this->block($def->fields) - ], ' '); + $def->description, + $this->join([ + 'type', + $def->name, + $this->wrap('implements ', $this->join($def->interfaces, ', ')), + $this->join($def->directives, ' '), + $this->block($def->fields) + ], ' ') + ], "\n"); }, NodeKind::FIELD_DEFINITION => function(FieldDefinitionNode $def) { - return $def->name + return $this->join([ + $def->description, + $def->name . $this->wrap('(', $this->join($def->arguments, ', '), ')') . ': ' . $def->type - . $this->wrap(' ', $this->join($def->directives, ' ')); + . $this->wrap(' ', $this->join($def->directives, ' ')) + ], "\n"); }, NodeKind::INPUT_VALUE_DEFINITION => function(InputValueDefinitionNode $def) { return $this->join([ - $def->name . ': ' . $def->type, - $this->wrap('= ', $def->defaultValue), - $this->join($def->directives, ' ') - ], ' '); + $def->description, + $this->join([ + $def->name . ': ' . $def->type, + $this->wrap('= ', $def->defaultValue), + $this->join($def->directives, ' ') + ], ' ') + ], "\n"); }, NodeKind::INTERFACE_TYPE_DEFINITION => function(InterfaceTypeDefinitionNode $def) { return $this->join([ - 'interface', - $def->name, - $this->join($def->directives, ' '), - $this->block($def->fields) - ], ' '); + $def->description, + $this->join([ + 'interface', + $def->name, + $this->join($def->directives, ' '), + $this->block($def->fields) + ], ' ') + ], "\n"); }, NodeKind::UNION_TYPE_DEFINITION => function(UnionTypeDefinitionNode $def) { return $this->join([ - 'union', - $def->name, - $this->join($def->directives, ' '), - '= ' . $this->join($def->types, ' | ') - ], ' '); + $def->description, + $this->join([ + 'union', + $def->name, + $this->join($def->directives, ' '), + '= ' . $this->join($def->types, ' | ') + ], ' ') + ], "\n"); }, NodeKind::ENUM_TYPE_DEFINITION => function(EnumTypeDefinitionNode $def) { return $this->join([ - 'enum', - $def->name, - $this->join($def->directives, ' '), - $this->block($def->values) - ], ' '); + $def->description, + $this->join([ + 'enum', + $def->name, + $this->join($def->directives, ' '), + $this->block($def->values) + ], ' ') + ], "\n"); }, NodeKind::ENUM_VALUE_DEFINITION => function(EnumValueDefinitionNode $def) { return $this->join([ - $def->name, - $this->join($def->directives, ' ') - ], ' '); + $def->description, + $this->join([$def->name, $this->join($def->directives, ' ')], ' ') + ], "\n"); }, NodeKind::INPUT_OBJECT_TYPE_DEFINITION => function(InputObjectTypeDefinitionNode $def) { return $this->join([ - 'input', - $def->name, - $this->join($def->directives, ' '), - $this->block($def->fields) - ], ' '); + $def->description, + $this->join([ + 'input', + $def->name, + $this->join($def->directives, ' '), + $this->block($def->fields) + ], ' ') + ], "\n"); }, NodeKind::TYPE_EXTENSION_DEFINITION => function(TypeExtensionDefinitionNode $def) { return "extend {$def->definition}"; }, NodeKind::DIRECTIVE_DEFINITION => function(DirectiveDefinitionNode $def) { - return 'directive @' . $def->name . $this->wrap('(', $this->join($def->arguments, ', '), ')') - . ' on ' . $this->join($def->locations, ' | '); + return $this->join([ + $def->description, + 'directive @' . $def->name . $this->wrap('(', $this->join($def->arguments, ', '), ')') + . ' on ' . $this->join($def->locations, ' | ') + ], "\n"); } ] ]); @@ -316,9 +343,13 @@ class Printer * 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) { - return ($value[0] === ' ' || $value[0] === "\t") && strpos($value, "\n") === false - ? '"""' . str_replace('"""', '\\"""', $value) . '"""' - : $this->indent("\"\"\"\n" . str_replace('"""', '\\"""', $value)) . "\n\"\"\""; + private function printBlockString($value, $isDescription) { + return (($value[0] === ' ' || $value[0] === "\t") && strpos($value, "\n") === false) + ? ('"""' . str_replace('"""', '\\"""', $value) . '"""') + : ( + $isDescription + ? ("\"\"\"\n" . str_replace('"""', '\\"""', $value) . "\n\"\"\"") + : ($this->indent("\"\"\"\n" . str_replace('"""', '\\"""', $value)) . "\n\"\"\"") + ); } } diff --git a/src/Language/Visitor.php b/src/Language/Visitor.php index 9fadc8c..9ddca60 100644 --- a/src/Language/Visitor.php +++ b/src/Language/Visitor.php @@ -133,17 +133,17 @@ class Visitor NodeKind::SCHEMA_DEFINITION => ['directives', 'operationTypes'], NodeKind::OPERATION_TYPE_DEFINITION => ['type'], - NodeKind::SCALAR_TYPE_DEFINITION => ['name', 'directives'], - NodeKind::OBJECT_TYPE_DEFINITION => ['name', 'interfaces', 'directives', 'fields'], - NodeKind::FIELD_DEFINITION => ['name', 'arguments', 'type', 'directives'], - NodeKind::INPUT_VALUE_DEFINITION => ['name', 'type', 'defaultValue', 'directives'], - NodeKind::INTERFACE_TYPE_DEFINITION => [ 'name', 'directives', 'fields' ], - NodeKind::UNION_TYPE_DEFINITION => [ 'name', 'directives', 'types' ], - NodeKind::ENUM_TYPE_DEFINITION => [ 'name', 'directives', 'values' ], - NodeKind::ENUM_VALUE_DEFINITION => [ 'name', 'directives' ], - NodeKind::INPUT_OBJECT_TYPE_DEFINITION => [ 'name', 'directives', 'fields' ], + NodeKind::SCALAR_TYPE_DEFINITION => ['description', 'name', 'directives'], + NodeKind::OBJECT_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'], + NodeKind::FIELD_DEFINITION => ['description', 'name', 'arguments', 'type', 'directives'], + NodeKind::INPUT_VALUE_DEFINITION => ['description', 'name', 'type', 'defaultValue', 'directives'], + NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'], + NodeKind::UNION_TYPE_DEFINITION => ['description', 'name', 'directives', 'types'], + NodeKind::ENUM_TYPE_DEFINITION => ['description', 'name', 'directives', 'values'], + NodeKind::ENUM_VALUE_DEFINITION => ['description', 'name', 'directives'], + NodeKind::INPUT_OBJECT_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'], NodeKind::TYPE_EXTENSION_DEFINITION => [ 'definition' ], - NodeKind::DIRECTIVE_DEFINITION => [ 'name', 'arguments', 'locations' ] + NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations'] ]; /** diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index d75a11f..078427a 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -41,7 +41,7 @@ class BuildSchema /** * @param Type $innerType * @param TypeNode $inputTypeNode - * @return Type + * @return Type */ private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode) { @@ -75,15 +75,21 @@ class BuildSchema * 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. + * + * * @api * @param DocumentNode $ast * @param callable $typeConfigDecorator * @return Schema * @throws Error */ - public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null) + public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = []) { - $builder = new self($ast, $typeConfigDecorator); + $builder = new self($ast, $typeConfigDecorator, $options); return $builder->buildSchema(); } @@ -92,14 +98,16 @@ class BuildSchema private $nodeMap; private $typeConfigDecorator; private $loadedTypeDefs; + private $options; - public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null) + public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = []) { $this->ast = $ast; $this->typeConfigDecorator = $typeConfigDecorator; $this->loadedTypeDefs = []; + $this->options = $options; } - + public function buildSchema() { $schemaDef = null; @@ -584,17 +592,28 @@ class BuildSchema } /** - * Given an ast node, returns its string description based on a contiguous - * block full-line of comments preceding it. + * Given an ast node, returns its string description. */ public function getDescription($node) + { + if ($node->description) { + return $node->description->value; + } + if (isset($this->options['commentDescriptions'])) { + $rawValue = $this->getLeadingCommentBlock($node); + if ($rawValue !== null) { + return BlockString::value("\n" . $rawValue); + } + } + } + + public function getLeadingCommentBlock($node) { $loc = $node->loc; if (!$loc || !$loc->startToken) { return ; } $comments = []; - $minSpaces = null; $token = $loc->startToken->prev; while ( $token && @@ -604,22 +623,17 @@ class BuildSchema $token->line !== $token->prev->line ) { $value = $token->value; - $spaces = $this->leadingSpaces($value); - if ($minSpaces === null || $spaces < $minSpaces) { - $minSpaces = $spaces; - } $comments[] = $value; $token = $token->prev; } - return implode("\n", array_map(function($comment) use ($minSpaces) { - return mb_substr(str_replace("\n", '', $comment), $minSpaces); - }, array_reverse($comments))); + + return implode("\n", array_reverse($comments)); } /** * A helper function to build a GraphQLSchema directly from a source * document. - * + * * @api * @param DocumentNode|Source|string $source * @param callable $typeConfigDecorator @@ -631,12 +645,6 @@ class BuildSchema return self::buildAST($doc, $typeConfigDecorator); } - // Count the number of spaces on the starting side of a string. - private function leadingSpaces($str) - { - return strlen($str) - strlen(ltrim($str)); - } - public function cannotExecuteSchema() { throw new Error( diff --git a/src/Utils/SchemaPrinter.php b/src/Utils/SchemaPrinter.php index 1e1d9cb..93c667e 100644 --- a/src/Utils/SchemaPrinter.php +++ b/src/Utils/SchemaPrinter.php @@ -19,15 +19,24 @@ use GraphQL\Type\Definition\Directive; class SchemaPrinter { /** + * Accepts options as a second argument: + * + * - commentDescriptions: + * Provide true to use preceding comments as the description. * @api * @param Schema $schema * @return string */ - public static function doPrint(Schema $schema) + public static function doPrint(Schema $schema, array $options = []) { - return self::printFilteredSchema($schema, function($n) { - return !self::isSpecDirective($n); - }, 'self::isDefinedType'); + return self::printFilteredSchema( + $schema, + function($n) { + return !self::isSpecDirective($n); + }, + 'self::isDefinedType', + $options + ); } /** @@ -35,9 +44,14 @@ class SchemaPrinter * @param Schema $schema * @return string */ - public static function printIntrosepctionSchema(Schema $schema) + public static function printIntrosepctionSchema(Schema $schema, array $options = []) { - return self::printFilteredSchema($schema, [__CLASS__, 'isSpecDirective'], [__CLASS__, 'isIntrospectionType']); + return self::printFilteredSchema( + $schema, + [__CLASS__, 'isSpecDirective'], + [__CLASS__, 'isIntrospectionType'], + $options + ); } private static function isSpecDirective($directiveName) @@ -70,7 +84,7 @@ class SchemaPrinter ); } - private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter) + private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter, $options) { $directives = array_filter($schema->getDirectives(), function($directive) use ($directiveFilter) { return $directiveFilter($directive->name); @@ -82,8 +96,8 @@ class SchemaPrinter return implode("\n\n", array_filter(array_merge( [self::printSchemaDefinition($schema)], - array_map('self::printDirective', $directives), - array_map('self::printType', $types) + array_map(function($directive) use ($options) { return self::printDirective($directive, $options); }, $directives), + array_map(function($type) use ($options) { return self::printType($type, $options); }, $types) ))) . "\n"; } @@ -112,7 +126,7 @@ class SchemaPrinter return "schema {\n" . implode("\n", $operationTypes) . "\n}"; } - + /** * GraphQL schema define root types for each type of operation. These types are * the same as any other type and can be named in any manner, however there is @@ -145,93 +159,93 @@ class SchemaPrinter return true; } - public static function printType(Type $type) + public static function printType(Type $type, array $options = []) { if ($type instanceof ScalarType) { - return self::printScalar($type); + return self::printScalar($type, $options); } else if ($type instanceof ObjectType) { - return self::printObject($type); + return self::printObject($type, $options); } else if ($type instanceof InterfaceType) { - return self::printInterface($type); + return self::printInterface($type, $options); } else if ($type instanceof UnionType) { - return self::printUnion($type); + return self::printUnion($type, $options); } else if ($type instanceof EnumType) { - return self::printEnum($type); + return self::printEnum($type, $options); } Utils::invariant($type instanceof InputObjectType); - return self::printInputObject($type); + return self::printInputObject($type, $options); } - private static function printScalar(ScalarType $type) + private static function printScalar(ScalarType $type, array $options) { - return self::printDescription($type) . "scalar {$type->name}"; + return self::printDescription($options, $type) . "scalar {$type->name}"; } - private static function printObject(ObjectType $type) + private static function printObject(ObjectType $type, array $options) { $interfaces = $type->getInterfaces(); $implementedInterfaces = !empty($interfaces) ? ' implements ' . implode(', ', array_map(function($i) { return $i->name; }, $interfaces)) : ''; - return self::printDescription($type) . + return self::printDescription($options, $type) . "type {$type->name}$implementedInterfaces {\n" . - self::printFields($type) . "\n" . + self::printFields($options, $type) . "\n" . "}"; } - private static function printInterface(InterfaceType $type) + private static function printInterface(InterfaceType $type, array $options) { - return self::printDescription($type) . + return self::printDescription($options, $type) . "interface {$type->name} {\n" . - self::printFields($type) . "\n" . + self::printFields($options, $type) . "\n" . "}"; } - private static function printUnion(UnionType $type) + private static function printUnion(UnionType $type, array $options) { - return self::printDescription($type) . + return self::printDescription($options, $type) . "union {$type->name} = " . implode(" | ", $type->getTypes()); } - private static function printEnum(EnumType $type) + private static function printEnum(EnumType $type, array $options) { - return self::printDescription($type) . + return self::printDescription($options, $type) . "enum {$type->name} {\n" . - self::printEnumValues($type->getValues()) . "\n" . + self::printEnumValues($type->getValues(), $options) . "\n" . "}"; } - private static function printEnumValues($values) + private static function printEnumValues($values, $options) { - return implode("\n", array_map(function($value, $i) { - return self::printDescription($value, ' ', !$i) . ' ' . + return implode("\n", array_map(function($value, $i) use ($options) { + return self::printDescription($options, $value, ' ', !$i) . ' ' . $value->name . self::printDeprecated($value); }, $values, array_keys($values))); } - private static function printInputObject(InputObjectType $type) + private static function printInputObject(InputObjectType $type, array $options) { $fields = array_values($type->getFields()); - return self::printDescription($type) . + return self::printDescription($options, $type) . "input {$type->name} {\n" . - implode("\n", array_map(function($f, $i) { - return self::printDescription($f, ' ', !$i) . ' ' . self::printInputValue($f); + implode("\n", array_map(function($f, $i) use ($options) { + return self::printDescription($options, $f, ' ', !$i) . ' ' . self::printInputValue($f); }, $fields, array_keys($fields))) . "\n" . "}"; } - private static function printFields($type) + private static function printFields($options, $type) { $fields = array_values($type->getFields()); - return implode("\n", array_map(function($f, $i) { - return self::printDescription($f, ' ', !$i) . ' ' . - $f->name . self::printArgs($f->args, ' ') . ': ' . + return implode("\n", array_map(function($f, $i) use ($options) { + return self::printDescription($options, $f, ' ', !$i) . ' ' . + $f->name . self::printArgs($options, $f->args, ' ') . ': ' . (string) $f->getType() . self::printDeprecated($f); }, $fields, array_keys($fields))); } - private static function printArgs($args, $indentation = '') + private static function printArgs($options, $args, $indentation = '') { if (count($args) === 0) { return ''; @@ -242,8 +256,8 @@ class SchemaPrinter return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')'; } - return "(\n" . implode("\n", array_map(function($arg, $i) use ($indentation) { - return self::printDescription($arg, ' ' . $indentation, !$i) . ' ' . $indentation . + return "(\n" . implode("\n", array_map(function($arg, $i) use ($indentation, $options) { + return self::printDescription($options, $arg, ' ' . $indentation, !$i) . ' ' . $indentation . self::printInputValue($arg); }, $args, array_keys($args))) . "\n" . $indentation . ')'; } @@ -257,10 +271,10 @@ class SchemaPrinter return $argDecl; } - private static function printDirective($directive) + private static function printDirective($directive, $options) { - return self::printDescription($directive) . - 'directive @' . $directive->name . self::printArgs($directive->args) . + return self::printDescription($options, $directive) . + 'directive @' . $directive->name . self::printArgs($options, $directive->args) . ' on ' . implode(' | ', $directive->locations); } @@ -277,34 +291,74 @@ class SchemaPrinter Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')'; } - private static function printDescription($def, $indentation = '', $firstInBlock = true) + private static function printDescription($options, $def, $indentation = '', $firstInBlock = true) { if (!$def->description) { return ''; } - $lines = explode("\n", $def->description); + $lines = self::descriptionLines($def->description, 120 - strlen($indentation)); + if (isset($options['commentDescriptions'])) { + return self::printDescriptionWithComments($lines, $indentation, $firstInBlock); + } + + $description = ($indentation && !$firstInBlock) ? "\n" : ''; + if (count($lines) === 1 && mb_strlen($lines[0]) < 70) { + $description .= $indentation . '"""' . self::escapeQuote($lines[0]) . "\"\"\"\n"; + return $description; + } + + $description .= $indentation . "\"\"\"\n"; + foreach ($lines as $line) { + $description .= $indentation . self::escapeQuote($line) . "\n"; + } + $description .= $indentation . "\"\"\"\n"; + + return $description; + } + + private static function escapeQuote($line) + { + return str_replace('"""', '\\"""', $line); + } + + private static function printDescriptionWithComments($lines, $indentation, $firstInBlock) + { $description = $indentation && !$firstInBlock ? "\n" : ''; foreach ($lines as $line) { if ($line === '') { $description .= $indentation . "#\n"; } else { - // For > 120 character long lines, cut at space boundaries into sublines - // of ~80 chars. - $sublines = self::breakLine($line, 120 - strlen($indentation)); - foreach ($sublines as $subline) { - $description .= $indentation . '# ' . $subline . "\n"; - } + $description .= $indentation . '# ' . $line . "\n"; } } + return $description; } - private static function breakLine($line, $len) + private static function descriptionLines($description, $maxLen) { + $lines = []; + $rawLines = explode("\n", $description); + foreach($rawLines as $line) { + if ($line === '') { + $lines[] = $line; + } else { + // For > 120 character long lines, cut at space boundaries into sublines + // of ~80 chars. + $sublines = self::breakLine($line, $maxLen); + foreach ($sublines as $subline) { + $lines[] = $subline; + } + } + } + return $lines; + } + + private static function breakLine($line, $maxLen) { - if (strlen($line) < $len + 5) { + if (strlen($line) < $maxLen + 5) { return [$line]; } - preg_match_all("/((?: |^).{15," . ($len - 40) . "}(?= |$))/", $line, $parts); + preg_match_all("/((?: |^).{15," . ($maxLen - 40) . "}(?= |$))/", $line, $parts); $parts = $parts[0]; return array_map(function($part) { return trim($part); diff --git a/tests/Language/SchemaParserTest.php b/tests/Language/SchemaParserTest.php index 7b0324e..81d8a3f 100644 --- a/tests/Language/SchemaParserTest.php +++ b/tests/Language/SchemaParserTest.php @@ -45,6 +45,93 @@ type Hello { $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } + /** + * @it parses type with description string + */ + public function testParsesTypeWithDescriptionString() + { + $body = ' +"Description" +type Hello { + world: String +}'; + $doc = Parser::parse($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; + + $expected = [ + 'kind' => NodeKind::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKind::OBJECT_TYPE_DEFINITION, + 'name' => $this->nameNode('Hello', $loc(20, 25)), + 'interfaces' => [], + 'directives' => [], + 'fields' => [ + $this->fieldNode( + $this->nameNode('world', $loc(30, 35)), + $this->typeNode('String', $loc(37, 43)), + $loc(30, 43) + ) + ], + 'loc' => $loc(1, 45), + 'description' => [ + 'kind' => NodeKind::STRING, + 'value' => 'Description', + 'loc' => $loc(1, 14), + 'block' => false + ] + ] + ], + 'loc' => $loc(0, 45) + ]; + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); + } + + /** + * @it parses type with description multi-linestring + */ + public function testParsesTypeWithDescriptionMultiLineString() + { + $body = ' +""" +Description +""" +# Even with comments between them +type Hello { + world: String +}'; + $doc = Parser::parse($body); + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; + + $expected = [ + 'kind' => NodeKind::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKind::OBJECT_TYPE_DEFINITION, + 'name' => $this->nameNode('Hello', $loc(60, 65)), + 'interfaces' => [], + 'directives' => [], + 'fields' => [ + $this->fieldNode( + $this->nameNode('world', $loc(70, 75)), + $this->typeNode('String', $loc(77, 83)), + $loc(70, 83) + ) + ], + 'loc' => $loc(1, 85), + 'description' => [ + 'kind' => NodeKind::STRING, + 'value' => 'Description', + 'loc' => $loc(1, 20), + 'block' => true + ] + ] + ], + 'loc' => $loc(0, 85) + ]; + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); + } + /** * @it Simple extension */ @@ -87,6 +174,20 @@ extend type Hello { $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } + /** + * @it Extension do not include descriptions + * @expectedException \GraphQL\Error\SyntaxError + * @expectedExceptionMessage Syntax Error GraphQL (2:1) + */ + public function testExtensionDoNotIncludeDescriptions() { + $body = ' +"Description" +extend type Hello { + world: String +}'; + Parser::parse($body); + } + /** * @it Simple non-null type */ @@ -664,47 +765,6 @@ input Hello { Parser::parse($body); } - /** - * @it Simple type - */ - public function testSimpleTypeDescriptionInComments() - { - $body = ' -# This is a simple type description. -# It is multiline *and includes formatting*. -type Hello { - # And this is a field description - world: String -}'; - $doc = Parser::parse($body); - $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; - - $fieldNode = $this->fieldNode( - $this->nameNode('world', $loc(134, 139)), - $this->typeNode('String', $loc(141, 147)), - $loc(134, 147) - ); - $fieldNode['description'] = " And this is a field description\n"; - $expected = [ - 'kind' => NodeKind::DOCUMENT, - 'definitions' => [ - [ - 'kind' => NodeKind::OBJECT_TYPE_DEFINITION, - 'name' => $this->nameNode('Hello', $loc(88, 93)), - 'interfaces' => [], - 'directives' => [], - 'fields' => [ - $fieldNode - ], - 'loc' => $loc(83, 149), - 'description' => " This is a simple type description.\n It is multiline *and includes formatting*.\n" - ] - ], - 'loc' => $loc(0, 149) - ]; - $this->assertEquals($expected, TestUtils::nodeToArray($doc)); - } - private function typeNode($name, $loc) { return [ diff --git a/tests/Language/SchemaPrinterTest.php b/tests/Language/SchemaPrinterTest.php index a649ced..3b9a098 100644 --- a/tests/Language/SchemaPrinterTest.php +++ b/tests/Language/SchemaPrinterTest.php @@ -56,6 +56,10 @@ class SchemaPrinterTest extends \PHPUnit_Framework_TestCase mutation: MutationType } +""" +This is a description +of the `Foo` type. +""" type Foo implements Bar { one: Type two(argument: InputType!): Type diff --git a/tests/Language/schema-kitchen-sink.graphql b/tests/Language/schema-kitchen-sink.graphql index 7771a35..4b3fbaa 100644 --- a/tests/Language/schema-kitchen-sink.graphql +++ b/tests/Language/schema-kitchen-sink.graphql @@ -8,6 +8,10 @@ schema { mutation: MutationType } +""" +This is a description +of the `Foo` type. +""" type Foo implements Bar { one: Type two(argument: InputType!): Type diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index 951d358..095f315 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -17,11 +17,11 @@ class BuildSchemaTest extends \PHPUnit_Framework_TestCase { // Describe: Schema Builder - private function cycleOutput($body) + private function cycleOutput($body, $options = []) { $ast = Parser::parse($body); - $schema = BuildSchema::buildAST($ast); - return "\n" . SchemaPrinter::doPrint($schema); + $schema = BuildSchema::buildAST($ast, null, $options); + return "\n" . SchemaPrinter::doPrint($schema, $options); } /** @@ -35,7 +35,7 @@ class BuildSchemaTest extends \PHPUnit_Framework_TestCase str: String } ')); - + $result = GraphQL::execute($schema, '{ str }', ['str' => 123]); $this->assertEquals($result['data'], ['str' => 123]); } @@ -110,6 +110,42 @@ type Hello { * @it Supports descriptions */ public function testSupportsDescriptions() + { + $body = ' +schema { + query: Hello +} + +"""This is a directive""" +directive @foo( + """It has an argument""" + arg: Int +) on FIELD + +"""With an enum""" +enum Color { + RED + + """Not a creative color""" + GREEN + BLUE +} + +"""What a great type""" +type Hello { + """And a field to boot""" + str: String +} +'; + + $output = $this->cycleOutput($body); + $this->assertEquals($body, $output); + } + + /** + * @it Supports descriptions + */ + public function testSupportsOptionForCommentDescriptions() { $body = ' schema { @@ -137,7 +173,7 @@ type Hello { str: String } '; - $output = $this->cycleOutput($body); + $output = $this->cycleOutput($body, [ 'commentDescriptions' => true ]); $this->assertEquals($body, $output); } @@ -1115,44 +1151,4 @@ type World implements Hello { $this->assertArrayHasKey('Hello', $types); $this->assertArrayHasKey('World', $types); } - - public function testScalarDescription() - { - $schemaDef = ' -# An ISO-8601 encoded UTC date string. -scalar Date - -type Query { - now: Date - test: String -} -'; - $q = ' -{ - __type(name: "Date") { - name - description - } - strType: __type(name: "String") { - name - description - } -} -'; - $schema = BuildSchema::build($schemaDef); - $result = GraphQL::executeQuery($schema, $q)->toArray(); - $expected = ['data' => [ - '__type' => [ - 'name' => 'Date', - 'description' => 'An ISO-8601 encoded UTC date string.' - ], - 'strType' => [ - 'name' => 'String', - 'description' => 'The `String` scalar type represents textual data, represented as UTF-8' . "\n" . - 'character sequences. The String type is most often used by GraphQL to'. "\n" . - 'represent free-form human-readable text.' - ] - ]]; - $this->assertEquals($expected, $result); - } } diff --git a/tests/Utils/SchemaPrinterTest.php b/tests/Utils/SchemaPrinterTest.php index 49314f0..0acf0c2 100644 --- a/tests/Utils/SchemaPrinterTest.php +++ b/tests/Utils/SchemaPrinterTest.php @@ -650,6 +650,257 @@ schema { query: Root } +""" +Directs the executor to include this field or fragment only when the `if` argument is true. +""" +directive @include( + """Included when true.""" + if: Boolean! +) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" +Directs the executor to skip this field or fragment when the `if` argument is true. +""" +directive @skip( + """Skipped when true.""" + if: Boolean! +) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +"""Marks an element of a GraphQL schema as no longer supported.""" +directive @deprecated( + """ + Explains why this element was deprecated, usually also including a suggestion + for how to access supported similar data. Formatted in + [Markdown](https://daringfireball.net/projects/markdown/). + """ + reason: String = "No longer supported" +) on FIELD_DEFINITION | ENUM_VALUE + +""" +A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. + +In some cases, you need to provide options to alter GraphQL's execution behavior +in ways field arguments will not suffice, such as conditionally including or +skipping a field. Directives provide this by describing additional information +to the executor. +""" +type __Directive { + name: String! + description: String + locations: [__DirectiveLocation!]! + args: [__InputValue!]! + onOperation: Boolean! @deprecated(reason: "Use `locations`.") + onFragment: Boolean! @deprecated(reason: "Use `locations`.") + onField: Boolean! @deprecated(reason: "Use `locations`.") +} + +""" +A Directive can be adjacent to many parts of the GraphQL language, a +__DirectiveLocation describes one such possible adjacencies. +""" +enum __DirectiveLocation { + """Location adjacent to a query operation.""" + QUERY + + """Location adjacent to a mutation operation.""" + MUTATION + + """Location adjacent to a subscription operation.""" + SUBSCRIPTION + + """Location adjacent to a field.""" + FIELD + + """Location adjacent to a fragment definition.""" + FRAGMENT_DEFINITION + + """Location adjacent to a fragment spread.""" + FRAGMENT_SPREAD + + """Location adjacent to an inline fragment.""" + INLINE_FRAGMENT + + """Location adjacent to a schema definition.""" + SCHEMA + + """Location adjacent to a scalar definition.""" + SCALAR + + """Location adjacent to an object type definition.""" + OBJECT + + """Location adjacent to a field definition.""" + FIELD_DEFINITION + + """Location adjacent to an argument definition.""" + ARGUMENT_DEFINITION + + """Location adjacent to an interface definition.""" + INTERFACE + + """Location adjacent to a union definition.""" + UNION + + """Location adjacent to an enum definition.""" + ENUM + + """Location adjacent to an enum value definition.""" + ENUM_VALUE + + """Location adjacent to an input object type definition.""" + INPUT_OBJECT + + """Location adjacent to an input object field definition.""" + INPUT_FIELD_DEFINITION +} + +""" +One possible value for a given Enum. Enum values are unique values, not a +placeholder for a string or numeric value. However an Enum value is returned in +a JSON response as a string. +""" +type __EnumValue { + name: String! + description: String + isDeprecated: Boolean! + deprecationReason: String +} + +""" +Object and Interface types are described by a list of Fields, each of which has +a name, potentially a list of arguments, and a return type. +""" +type __Field { + name: String! + description: String + args: [__InputValue!]! + type: __Type! + isDeprecated: Boolean! + deprecationReason: String +} + +""" +Arguments provided to Fields or Directives and the input fields of an +InputObject are represented as Input Values which describe their type and +optionally a default value. +""" +type __InputValue { + name: String! + description: String + type: __Type! + + """ + A GraphQL-formatted string representing the default value for this input value. + """ + defaultValue: String +} + +""" +A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all +available types and directives on the server, as well as the entry points for +query, mutation, and subscription operations. +""" +type __Schema { + """A list of all types supported by this server.""" + types: [__Type!]! + + """The type that query operations will be rooted at.""" + queryType: __Type! + + """ + If this server supports mutation, the type that mutation operations will be rooted at. + """ + mutationType: __Type + + """ + If this server support subscription, the type that subscription operations will be rooted at. + """ + subscriptionType: __Type + + """A list of all directives supported by this server.""" + directives: [__Directive!]! +} + +""" +The fundamental unit of any GraphQL Schema is the type. There are many kinds of +types in GraphQL as represented by the `__TypeKind` enum. + +Depending on the kind of a type, certain fields describe information about that +type. Scalar types provide no information beyond a name and description, while +Enum types provide their values. Object and Interface types provide the fields +they describe. Abstract types, Union and Interface, provide the Object types +possible at runtime. List and NonNull types compose other types. +""" +type __Type { + kind: __TypeKind! + name: String + description: String + fields(includeDeprecated: Boolean = false): [__Field!] + interfaces: [__Type!] + possibleTypes: [__Type!] + enumValues(includeDeprecated: Boolean = false): [__EnumValue!] + inputFields: [__InputValue!] + ofType: __Type +} + +"""An enum describing what kind of type a given `__Type` is.""" +enum __TypeKind { + """Indicates this type is a scalar.""" + SCALAR + + """ + Indicates this type is an object. `fields` and `interfaces` are valid fields. + """ + OBJECT + + """ + Indicates this type is an interface. `fields` and `possibleTypes` are valid fields. + """ + INTERFACE + + """Indicates this type is a union. `possibleTypes` is a valid field.""" + UNION + + """Indicates this type is an enum. `enumValues` is a valid field.""" + ENUM + + """ + Indicates this type is an input object. `inputFields` is a valid field. + """ + INPUT_OBJECT + + """Indicates this type is a list. `ofType` is a valid field.""" + LIST + + """Indicates this type is a non-null. `ofType` is a valid field.""" + NON_NULL +} + +EOT; + $this->assertEquals($introspectionSchema, $output); + } + + /** + * @it Print Introspection Schema with comment description + */ + public function testPrintIntrospectionSchemaWithCommentDescription() + { + $root = new ObjectType([ + 'name' => 'Root', + 'fields' => [ + 'onlyField' => ['type' => Type::string()] + ] + ]); + + $schema = new Schema(['query' => $root]); + $output = SchemaPrinter::printIntrosepctionSchema($schema, [ + 'commentDescriptions' => true + ]); + $introspectionSchema = <<<'EOT' +schema { + query: Root +} + # Directs the executor to include this field or fragment only when the `if` argument is true. directive @include( # Included when true. @@ -845,6 +1096,6 @@ enum __TypeKind { } EOT; - $this->assertEquals($output, $introspectionSchema); + $this->assertEquals($introspectionSchema, $output); } } \ No newline at end of file