diff --git a/src/SchemaPrinter/Blocks/Ast/ArgumentNodeList.php b/src/SchemaPrinter/Blocks/Ast/ArgumentNodeList.php new file mode 100644 index 00000000..fc23a871 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Ast/ArgumentNodeList.php @@ -0,0 +1,53 @@ +> + */ +class ArgumentNodeList extends BlockList { + /** + * @param Traversable|array $arguments + */ + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + Traversable|array $arguments, + ) { + parent::__construct($settings, $level, $used); + + foreach ($arguments as $argument) { + $name = $argument->name->value; + $this[$name] = new Property( + $this->getSettings(), + $name, + new ValueNodeBlock( + $this->getSettings(), + $this->getLevel() + 1, + $this->getUsed(), + $argument->value, + ), + ); + } + } + + protected function getPrefix(): string { + return '('; + } + + protected function getSuffix(): string { + return ')'; + } + + protected function isNormalized(): bool { + return $this->getSettings()->isNormalizeArguments(); + } +} diff --git a/src/SchemaPrinter/Blocks/Ast/DirectiveNodeBlock.php b/src/SchemaPrinter/Blocks/Ast/DirectiveNodeBlock.php new file mode 100644 index 00000000..6a7e7266 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Ast/DirectiveNodeBlock.php @@ -0,0 +1,53 @@ +getNode()->name->value}"; + } + + public function getNode(): DirectiveNode { + return $this->node; + } + + protected function content(): string { + // Convert + $node = $this->getNode(); + $name = $this->getName(); + $used = mb_strlen($name); + $args = $this->addUsed( + new ArgumentNodeList( + $this->getSettings(), + $this->getLevel(), + $this->getUsed() + $used, + $node->arguments, + ), + ); + + // Statistics + $this->addUsedDirective($name); + + // Return + return "{$name}{$args}"; + } +} diff --git a/src/SchemaPrinter/Blocks/Ast/DirectiveNodeBlockTest.php b/src/SchemaPrinter/Blocks/Ast/DirectiveNodeBlockTest.php new file mode 100644 index 00000000..faf4653c --- /dev/null +++ b/src/SchemaPrinter/Blocks/Ast/DirectiveNodeBlockTest.php @@ -0,0 +1,127 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + DirectiveNode $node, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new DirectiveNodeBlock($settings, $level, $used, $node)); + $parsed = Parser::directive($actual); + + self::assertEquals($expected, $actual); + + if (!$settings->isNormalizeArguments()) { + self::assertEquals( + Printer::doPrint($node), + Printer::doPrint($parsed), + ); + } + } + + /** + * @covers ::__toString + */ + public function testStatistics(): void { + $settings = new TestSettings(); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $node = Parser::directive('@test'); + $block = new DirectiveNodeBlock($settings, 0, 0, $node); + + self::assertNotEmpty((string) $block); + self::assertEquals([], $block->getUsedTypes()); + self::assertEquals(['@test' => '@test'], $block->getUsedDirectives()); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = (new TestSettings()) + ->setNormalizeArguments(false); + + return [ + 'without arguments' => [ + '@directive', + $settings, + 0, + 0, + Parser::directive('@directive'), + ], + 'without arguments (level)' => [ + '@directive', + $settings, + 0, + 0, + Parser::directive('@directive'), + ], + 'with arguments (short)' => [ + '@directive(a: "a", b: "b")', + $settings, + 0, + 0, + Parser::directive('@directive(a: "a", b: "b")'), + ], + 'with arguments (long)' => [ + <<<'STRING' + @directive( + b: "b" + a: "a" + ) + STRING, + $settings, + 0, + 120, + Parser::directive('@directive(b: "b", a: "a")'), + ], + 'with arguments (normalized)' => [ + '@directive(a: "a", b: "b")', + $settings->setNormalizeArguments(true), + 0, + 0, + Parser::directive('@directive(b: "b", a: "a")'), + ], + 'with arguments (indent)' => [ + <<<'STRING' + @directive( + b: "b" + a: "a" + ) + STRING, + $settings, + 1, + 120, + Parser::directive('@directive(b: "b", a: "a")'), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Ast/DirectiveNodeList.php b/src/SchemaPrinter/Blocks/Ast/DirectiveNodeList.php new file mode 100644 index 00000000..d5b7c0d8 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Ast/DirectiveNodeList.php @@ -0,0 +1,81 @@ + + */ +class DirectiveNodeList extends BlockList { + /** + * @param Traversable|array $directives + */ + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + Traversable|array|null $directives, + string|null $deprecationReason = null, + ) { + parent::__construct($settings, $level, $used); + + $deprecated = Directive::DEPRECATED_NAME; + $directives ??= []; + + if ($deprecationReason) { + // todo(graphql): Is there a better way to create directive node? + if ($deprecationReason !== Directive::DEFAULT_DEPRECATION_REASON) { + $reason = json_encode($deprecationReason); + $this[] = $this->block(Parser::directive("@{$deprecated}(reason: {$reason})")); + } else { + $this[] = $this->block(Parser::directive("@{$deprecated}")); + } + } + + foreach ($directives as $directive) { + if ($deprecationReason && $directive->name->value === $deprecated) { + continue; + } + + $this[] = $this->block($directive); + } + } + + protected function isAlwaysMultiline(): bool { + return true; + } + + protected function isValidBlock(Block $value): bool { + // Parent? + if (!parent::isValidBlock($value)) { + return false; + } + + // Allowed? + $settings = $this->getSettings(); + $filter = $settings->getDirectiveFilter(); + $valid = $filter === null + || $filter->isAllowedDirective($settings->getDirective($value->getNode())); + + return $valid; + } + + private function block(DirectiveNode $directive,): DirectiveNodeBlock { + return new DirectiveNodeBlock( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $directive, + ); + } +} diff --git a/src/SchemaPrinter/Blocks/Ast/DirectiveNodeListTest.php b/src/SchemaPrinter/Blocks/Ast/DirectiveNodeListTest.php new file mode 100644 index 00000000..0d85ca22 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Ast/DirectiveNodeListTest.php @@ -0,0 +1,207 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + * + * @param array $directives + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + array|null $directives, + string $reason = null, + ): void { + $locator = $this->app->make(DirectiveLocator::class); + $convertor = $this->app->make(ExecutableTypeNodeConverter::class); + $instances = []; + + foreach ((array) $directives as $directive) { + $instances[] = new GraphQLDirective([ + 'name' => $directive->name->value, + 'locations' => [DirectiveLocation::OBJECT], + ]); + } + + $resolver = new DirectiveResolver($locator, $convertor, $instances); + $settings = new PrinterSettings($resolver, $settings); + $actual = (string) (new DirectiveNodeList($settings, $level, $used, $directives, $reason)); + + Parser::directives($actual); + + self::assertEquals($expected, $actual); + } + + /** + * @covers ::__toString + */ + public function testStatistics(): void { + $a = Parser::directive('@a'); + $b = Parser::directive('@b'); + $settings = (new TestSettings())->setPrintDirectives(true); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $block = new DirectiveNodeList($settings, 0, 0, [$a, $b]); + + self::assertNotEmpty((string) $block); + self::assertEquals([], $block->getUsedTypes()); + self::assertEquals(['@a' => '@a', '@b' => '@b'], $block->getUsedDirectives()); + } + // + + // + // ========================================================================= + /** + * @return array, ?string}> + */ + public function dataProviderToString(): array { + $settings = new TestSettings(); + + return [ + 'null' => [ + '', + $settings, + 0, + 0, + null, + null, + ], + 'empty' => [ + '', + $settings, + 0, + 0, + [], + null, + ], + 'directives' => [ + <<<'STRING' + @b(b: 123) + @a + STRING, + $settings, + 0, + 0, + [ + Parser::directive('@b(b: 123)'), + Parser::directive('@a'), + ], + null, + ], + 'deprecated (default reason)' => [ + <<<'STRING' + @deprecated + STRING, + $settings, + 0, + 0, + null, + GraphQLDirective::DEFAULT_DEPRECATION_REASON, + ], + 'deprecated (custom reason)' => [ + <<<'STRING' + @deprecated(reason: "reason") + STRING, + $settings, + 0, + 0, + null, + 'reason', + ], + 'directives and deprecated (custom reason)' => [ + <<<'STRING' + @deprecated(reason: "reason") + @b(b: 123) + STRING, + $settings, + 0, + 0, + [ + Parser::directive('@b(b: 123)'), + Parser::directive('@deprecated(reason: "should be ignored")'), + ], + 'reason', + ], + 'line length' => [ + <<<'STRING' + @deprecated( + reason: "very very very long reason" + ) + @a(a: 123) + @b( + b: 1234567890 + ) + STRING, + $settings, + 0, + 70, + [ + Parser::directive('@a(a: 123)'), + Parser::directive('@b(b: 1234567890)'), + ], + 'very very very long reason', + ], + 'indent' => [ + <<<'STRING' + @deprecated( + reason: "very very very long reason" + ) + @a(a: 123) + @b( + b: 1234567890 + ) + STRING, + $settings, + 1, + 70, + [ + Parser::directive('@a(a: 123)'), + Parser::directive('@b(b: 1234567890)'), + ], + 'very very very long reason', + ], + 'filter' => [ + <<<'STRING' + @a(a: 123) + STRING, + $settings->setDirectiveFilter(static function (GraphQLDirective|LighthouseDirective $directive): bool { + return $directive instanceof GraphQLDirective + && $directive->name === 'a'; + }), + 0, + 0, + [ + Parser::directive('@a(a: 123)'), + Parser::directive('@b(b: 1234567890)'), + Parser::directive('@c'), + ], + null, + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Ast/ListValueList.php b/src/SchemaPrinter/Blocks/Ast/ListValueList.php new file mode 100644 index 00000000..6de64f80 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Ast/ListValueList.php @@ -0,0 +1,25 @@ + + */ +class ListValueList extends BlockList { + protected function getPrefix(): string { + return '['; + } + + protected function getSuffix(): string { + return ']'; + } + + protected function getEmptyValue(): string { + return "{$this->getPrefix()}{$this->getSuffix()}"; + } +} diff --git a/src/SchemaPrinter/Blocks/Ast/ObjectValueList.php b/src/SchemaPrinter/Blocks/Ast/ObjectValueList.php new file mode 100644 index 00000000..3ed2cc2b --- /dev/null +++ b/src/SchemaPrinter/Blocks/Ast/ObjectValueList.php @@ -0,0 +1,33 @@ + + */ +class ObjectValueList extends BlockList { + protected function getPrefix(): string { + return '{'; + } + + protected function getSuffix(): string { + return '}'; + } + + protected function getEmptyValue(): string { + return "{$this->getPrefix()}{$this->getSuffix()}"; + } + + protected function isAlwaysMultiline(): bool { + return true; + } + + protected function isNormalized(): bool { + return $this->getSettings()->isNormalizeArguments(); + } +} diff --git a/src/SchemaPrinter/Blocks/Ast/ValueNodeBlock.php b/src/SchemaPrinter/Blocks/Ast/ValueNodeBlock.php new file mode 100644 index 00000000..84d7ca4d --- /dev/null +++ b/src/SchemaPrinter/Blocks/Ast/ValueNodeBlock.php @@ -0,0 +1,67 @@ +getSettings(); + $level = $this->getLevel(); + $used = $this->getUsed(); + + if ($this->node instanceof ListValueNode) { + $content = new ListValueList($settings, $level, $used); + + foreach ($this->node->values as $value) { + $content[] = new ValueNodeBlock($settings, $level + 1, $used, $value); + } + } elseif ($this->node instanceof ObjectValueNode) { + $content = new ObjectValueList($settings, $level, $used); + + foreach ($this->node->fields as $field) { + $name = $field->name->value; + $content[$name] = new Property( + $settings, + $name, + new ValueNodeBlock( + $settings, + $level + 1 + (int) ($field->value instanceof StringValueNode), + $used, + $field->value, + ), + ); + } + } elseif ($this->node instanceof StringValueNode) { + $content = $this->node->block + ? new StringBlock($settings, $level, 0, $this->node->value) + : Printer::doPrint($this->node); + } else { + $content = Printer::doPrint($this->node); + } + + return (string) $this->addUsed($content); + } +} diff --git a/src/SchemaPrinter/Blocks/Ast/ValueNodeTest.php b/src/SchemaPrinter/Blocks/Ast/ValueNodeTest.php new file mode 100644 index 00000000..2553adea --- /dev/null +++ b/src/SchemaPrinter/Blocks/Ast/ValueNodeTest.php @@ -0,0 +1,297 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + * + * @param ValueNode&Node $node + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + ValueNode $node, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new ValueNodeBlock($settings, $level, $used, $node)); + $parsed = Parser::valueLiteral($actual); + + self::assertEquals($expected, $actual); + + if (!$settings->isNormalizeArguments()) { + self::assertEquals( + Printer::doPrint($node), + Printer::doPrint($parsed), + ); + } + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = (new TestSettings()) + ->setNormalizeArguments(false); + + return [ + NullValueNode::class => [ + 'null', + $settings, + 0, + 0, + Parser::valueLiteral('null'), + ], + IntValueNode::class => [ + '123', + $settings, + 0, + 0, + Parser::valueLiteral('123'), + ], + FloatValueNode::class => [ + '123.45', + $settings, + 0, + 0, + Parser::valueLiteral('123.45'), + ], + BooleanValueNode::class => [ + 'true', + $settings, + 0, + 0, + Parser::valueLiteral('true'), + ], + StringValueNode::class => [ + '"true"', + $settings, + 0, + 0, + Parser::valueLiteral('"true"'), + ], + EnumValueNode::class => [ + 'Value', + $settings, + 0, + 0, + Parser::valueLiteral('Value'), + ], + VariableNode::class => [ + '$variable', + $settings, + 0, + 0, + Parser::valueLiteral('$variable'), + ], + ListValueNode::class.' (short)' => [ + '["a", "b", "c"]', + $settings, + 0, + 0, + Parser::valueLiteral('["a", "b", "c"]'), + ], + ListValueNode::class.' (with block string)' => [ + <<<'STRING' + [ + "string" + """ + Block + string + """ + ] + STRING, + $settings, + 0, + 0, + Parser::valueLiteral( + <<<'STRING' + [ + "string" + """ + Block + string + """ + ] + STRING, + ), + ], + ListValueNode::class.' (with block string and level)' => [ + <<<'STRING' + [ + "string" + """ + Block + string + """ + ] + STRING, + $settings, + 1, + 0, + Parser::valueLiteral( + <<<'STRING' + [ + "string" + """ + Block + string + """ + ] + STRING, + ), + ], + ListValueNode::class.' (empty)' => [ + <<<'STRING' + [] + STRING, + $settings, + 1, + 0, + Parser::valueLiteral('[]'), + ], + ObjectValueNode::class => [ + <<<'STRING' + { + object: { + a: "a" + b: "b" + } + } + STRING, + $settings, + 0, + 0, + Parser::valueLiteral( + <<<'STRING' + { + object: { + a: "a" + b: "b" + } + } + STRING, + ), + ], + ObjectValueNode::class.' (empty)' => [ + <<<'STRING' + {} + STRING, + $settings, + 0, + 0, + Parser::valueLiteral('{}'), + ], + ObjectValueNode::class.' (normalized)' => [ + <<<'STRING' + { + a: "a" + b: "b" + } + STRING, + $settings + ->setNormalizeArguments(true), + 0, + 0, + Parser::valueLiteral( + <<<'STRING' + { + b: "b" + a: "a" + } + STRING, + ), + ], + 'all' => [ + <<<'STRING' + { + int: 123 + bool: true + string: "string" + blockString: """ + Block + string + """ + array: [1, 2, 3] + object: { + a: "a" + b: { + b: null + array: [3] + nested: { + a: 123 + } + } + } + } + STRING, + $settings, + 0, + 0, + Parser::valueLiteral( + <<<'STRING' + { + int: 123 + bool: true + string: "string" + blockString: """ + Block + string + """ + array: [ + 1 + 2 + 3 + ] + object: { + a: "a" + b: { + b: null + array: [ + 3 + ] + nested: { + a: 123 + } + } + } + } + STRING, + ), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Block.php b/src/SchemaPrinter/Blocks/Block.php new file mode 100644 index 00000000..bc274e39 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Block.php @@ -0,0 +1,179 @@ + + */ + private array $usedTypes = []; + + /** + * @var array + */ + private array $usedDirectives = []; + + public function __construct( + private PrinterSettings $settings, + private int $level = 0, + private int $used = 0, + ) { + // empty + } + + // + // ========================================================================= + protected function getSettings(): PrinterSettings { + return $this->settings; + } + + protected function getLevel(): int { + return $this->level; + } + + protected function getUsed(): int { + return $this->used; + } + // + + // + // ========================================================================= + public function isEmpty(): bool { + return $this->getLength() <= 0; + } + + public function getLength(): int { + return $this->resolve(fn (): int => (int) $this->length); + } + + public function isMultiline(): bool { + return $this->resolve(fn (): bool => (bool) $this->multiline); + } + + public function __toString(): string { + return $this->getContent(); + } + + /** + * @template T + * + * @param Closure(string $content): T $callback + * + * @return T + */ + protected function resolve(Closure $callback): mixed { + $content = $this->getContent(); + $result = $callback($content); + + return $result; + } + // + + // + // ========================================================================= + protected function getContent(): string { + if ($this->content === null) { + $this->content = $this->content(); + $this->length = mb_strlen($this->content); + $this->multiline = $this->isStringMultiline($this->content); + } + + return $this->content; + } + + protected function reset(): void { + $this->usedDirectives = []; + $this->usedTypes = []; + $this->multiline = null; + $this->content = null; + $this->length = null; + } + + abstract protected function content(): string; + // + + // + // ========================================================================= + protected function eol(): string { + return $this->getSettings()->getLineEnd(); + } + + protected function space(): string { + return $this->getSettings()->getSpace(); + } + + protected function indent(int $level = null): string { + return str_repeat($this->getSettings()->getIndent(), $level ?? $this->getLevel()); + } + + protected function isLineTooLong(int $length): bool { + return $length > $this->getSettings()->getLineLength(); + } + + protected function isStringMultiline(string $string): bool { + return mb_strpos($string, "\n") !== false + || mb_strpos($string, "\r") !== false; + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function getUsedTypes(): array { + return $this->resolve(fn (): array => $this->usedTypes); + } + + /** + * @return array + */ + public function getUsedDirectives(): array { + return $this->resolve(fn (): array => $this->usedDirectives); + } + + /** + * @template T + * + * @param T $block + * + * @return T + */ + protected function addUsed(mixed $block): mixed { + if ($block instanceof Statistics) { + $this->usedTypes += $block->getUsedTypes(); + $this->usedDirectives += $block->getUsedDirectives(); + } + + return $block; + } + + protected function addUsedType(string $type): static { + $this->usedTypes[$type] = $type; + + return $this; + } + + protected function addUsedDirective(string $directive): static { + $this->usedDirectives[$directive] = $directive; + + return $this; + } + // +} diff --git a/src/SchemaPrinter/Blocks/BlockList.php b/src/SchemaPrinter/Blocks/BlockList.php new file mode 100644 index 00000000..0ec315f2 --- /dev/null +++ b/src/SchemaPrinter/Blocks/BlockList.php @@ -0,0 +1,281 @@ + + */ +abstract class BlockList extends Block implements Statistics, ArrayAccess, Countable { + /** + * @var array + */ + private array $blocks = []; + + /** + * @var array + */ + private array $multiline = []; + private int $length = 0; + + // + // ========================================================================= + protected function isWrapped(): bool { + return false; + } + + protected function isNormalized(): bool { + return false; + } + + protected function isAlwaysMultiline(): bool { + return false; + } + + protected function getPrefix(): string { + return ''; + } + + protected function getSuffix(): string { + return ''; + } + + protected function getSeparator(): string { + return ",{$this->space()}"; + } + + protected function getMultilineItemPrefix(): string { + return ''; + } + + protected function getEmptyValue(): string { + return ''; + } + // + + // + // ========================================================================= + public function isEmpty(): bool { + return count($this->blocks) === 0 || parent::isEmpty(); + } + + public function isMultiline(): bool { + return count($this->multiline) > 0 || parent::isMultiline(); + } + // + + // + // ========================================================================= + /** + * @return array + */ + protected function getBlocks(): array { + $blocks = $this->blocks; + + if (count($blocks) > 0 && $this->isNormalized()) { + usort($blocks, static function (Block $a, Block $b): int { + $aName = $a instanceof Named ? $a->getName() : ''; + $bName = $b instanceof Named ? $b->getName() : ''; + + return strnatcmp($aName, $bName); + }); + } + + return $blocks; + } + + protected function content(): string { + // Blocks? + $content = ''; + $blocks = $this->getBlocks(); + $count = count($blocks); + + if (!$count) { + return $this->getEmptyValue(); + } + + // Join + $listPrefix = $this->getPrefix(); + $listSuffix = $this->getSuffix(); + $separator = $this->getSeparator(); + $isWrapped = (bool) $listPrefix || (bool) $listSuffix; + $isMultiline = $this->isMultilineContent( + $blocks, + $listSuffix, + $listPrefix, + $separator, + ); + + if ($isMultiline) { + $eol = $this->eol(); + $last = $count - 1; + $index = 0; + $indent = $this->indent($this->getLevel() + (int) $isWrapped); + $wrapped = $this->isWrapped(); + $previous = false; + $separator = $this->getMultilineItemPrefix(); + + foreach ($blocks as $block) { + $block = $this->analyze($block); + $multiline = $wrapped && $block->isMultiline(); + + if (($multiline && $index > 0) || $previous) { + $content .= $eol; + } + + if ($index > 0 || $isWrapped) { + $content .= $indent; + } + + $content .= "{$separator}{$block}"; + + if ($index < $last) { + $content .= $eol; + } + + $previous = $multiline; + $index = $index + 1; + } + } else { + $last = $count - 1; + $index = 0; + + foreach ($blocks as $block) { + $content .= "{$this->analyze($block)}".($index !== $last ? $separator : ''); + $index = $index + 1; + } + } + + // Prefix & Suffix + if ($isWrapped) { + $eol = $isMultiline ? $this->eol() : ''; + $indent = $isMultiline ? $this->indent() : ''; + $content = "{$listPrefix}{$eol}{$content}"; + + if ($listSuffix) { + $content .= "{$eol}{$indent}{$listSuffix}"; + } + } + + // Return + return $content; + } + + /** + * @param array $blocks + */ + private function isMultilineContent( + array $blocks, + string $suffix, + string $prefix, + string $separator, + ): bool { + // Always or Any multiline block? + if ($this->isAlwaysMultiline() || count($this->multiline) > 0) { + return true; + } + + // Length? + $count = count($blocks); + $length = $this->getUsed() + + $this->length + + mb_strlen($suffix) + + mb_strlen($prefix) + + mb_strlen($separator) * ($count - 1); + + return $this->isLineTooLong($length); + } + + /** + * @param TBlock $block + * + * @return TBlock + */ + protected function analyze(Block $block): Block { + return $this->addUsed($block); + } + + /** + * @param TBlock $value + */ + protected function isValidBlock(Block $value): bool { + return !$value->isEmpty(); + } + // + + // + // ========================================================================= + /** + * @param int|string $offset + */ + public function offsetExists(mixed $offset): bool { + return isset($this->blocks[$offset]); + } + + /** + * @param int|string $offset + * + * @return TBlock + */ + public function offsetGet(mixed $offset): Block { + return $this->blocks[$offset]; + } + + /** + * @param int|string|null $offset + * @param TBlock $value + */ + public function offsetSet(mixed $offset, mixed $value): void { + if (!$this->isValidBlock($value)) { + return; + } + + if ($offset !== null) { + $this->blocks[$offset] = $value; + } else { + $this->blocks[] = $value; + $offset = array_key_last($this->blocks); + } + + $this->length += $value->getLength(); + + if ($value->isMultiline()) { + $this->multiline[$offset] = true; + } + + $this->reset(); + } + + /** + * @param int|string $offset + */ + public function offsetUnset(mixed $offset): void { + if (isset($this->blocks[$offset])) { + $this->length -= $this->blocks[$offset]->getLength(); + } + + unset($this->blocks[$offset]); + unset($this->multiline[$offset]); + + $this->reset(); + } + // + + // + // ========================================================================= + public function count(): int { + return count($this->blocks); + } + // +} diff --git a/src/SchemaPrinter/Blocks/BlockListTest.php b/src/SchemaPrinter/Blocks/BlockListTest.php new file mode 100644 index 00000000..2fe01525 --- /dev/null +++ b/src/SchemaPrinter/Blocks/BlockListTest.php @@ -0,0 +1,838 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + * + * @param array $blocks + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + bool $normalized, + bool $wrapped, + string $prefix, + string $suffix, + string $separator, + string $multilineSeparator, + array $blocks, + int $count, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $list = new BlockListTest__BlockList( + $settings, + $level, + $used, + $normalized, + $wrapped, + $prefix, + $suffix, + $separator, + $multilineSeparator, + ); + + foreach ($blocks as $name => $block) { + $list[$name] = $block; + } + + self::assertEquals($expected, (string) $list); + self::assertCount($count, $list); + } + + /** + * @covers ::content + * @covers ::analyze + */ + public function testStatistics(): void { + $settings = new TestSettings(); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $list = new class($settings) extends BlockList { + // empty + }; + $list[] = new BlockListTest__StatisticsBlock(['ta'], ['da']); + $list[] = new BlockListTest__StatisticsBlock(['tb'], ['db']); + + self::assertEquals(['ta' => 'ta', 'tb' => 'tb'], $list->getUsedTypes()); + self::assertEquals(['da' => 'da', 'db' => 'db'], $list->getUsedDirectives()); + } + // + + // + // ========================================================================= + /** + * @return array> + */ + public function dataProviderToString(): array { + $settings = new TestSettings(); + + return (new MergeDataProvider([ + 'index' => new ArrayDataProvider([ + 'one single-line block' => [ + <<<'STRING' + block a + STRING, + $settings, + 0, + 0, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__Block(false, 'block a'), + ], + 1, + ], + 'one multi-line block' => [ + <<<'STRING' + block a + STRING, + $settings, + 0, + 0, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__Block(true, 'block a'), + ], + 1, + ], + 'short block list' => [ + <<<'STRING' + block a, block b + STRING, + $settings, + 0, + 0, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__Block(false, 'block a'), + new BlockListTest__Block(false, 'block b'), + ], + 2, + ], + 'long block list' => [ + <<<'STRING' + block b + block a + STRING, + $settings->setLineLength(20), + 0, + 5, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__Block(false, 'block b'), + new BlockListTest__Block(false, 'block a'), + ], + 2, + ], + 'short block list with multiline block' => [ + <<<'STRING' + block a + + block b + STRING, + $settings, + 0, + 0, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__Block(false, 'block a'), + new BlockListTest__Block(true, 'block b'), + ], + 2, + ], + 'block list with multiline blocks' => [ + <<<'STRING' + block a + + block b + block c + + block d + + block e + block f + + block g + STRING, + $settings, + 0, + 0, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__Block(true, 'block a'), + new BlockListTest__Block(false, 'block b'), + new BlockListTest__Block(false, 'block c'), + new BlockListTest__Block(true, 'block d'), + new BlockListTest__Block(false, 'block e'), + new BlockListTest__Block(false, 'block f'), + new BlockListTest__Block(true, 'block g'), + ], + 7, + ], + 'block list with multiline blocks without wrap' => [ + <<<'STRING' + block c + block b + block a + STRING, + $settings, + 0, + 0, + false, + false, + '', + '', + ', ', + '', + [ + new BlockListTest__Block(true, 'block c'), + new BlockListTest__Block(false, 'block b'), + new BlockListTest__Block(true, 'block a'), + ], + 3, + ], + 'normalized block list' => [ + <<<'STRING' + block b, block a + STRING, + $settings, + 0, + 0, + true, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__Block(false, 'block b'), + new BlockListTest__Block(false, 'block a'), + ], + 2, + ], + 'multi-line with level' => [ + <<<'STRING' + block a + block b + STRING, + $settings->setIndent(' '), + 2, + 0, + false, + false, + '', + '', + ', ', + '', + [ + new BlockListTest__Block(true, 'block a'), + new BlockListTest__Block(true, 'block b'), + ], + 2, + ], + ]), + 'named' => new ArrayDataProvider([ + 'one single-line block' => [ + <<<'STRING' + a: block a + STRING, + $settings, + 0, + 0, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__NamedBlock('a', false, 'block a'), + ], + 1, + ], + 'one multi-line block' => [ + <<<'STRING' + a: block a + STRING, + $settings, + 0, + 0, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__NamedBlock('a', true, 'block a'), + ], + 1, + ], + 'short block list' => [ + <<<'STRING' + a: block a, b: block b + STRING, + $settings, + 0, + 0, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__NamedBlock('a', false, 'block a'), + new BlockListTest__NamedBlock('b', false, 'block b'), + ], + 2, + ], + 'long block list' => [ + <<<'STRING' + b: block b + a: block a + STRING, + $settings->setLineLength(20), + 0, + 5, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__NamedBlock('b', false, 'block b'), + new BlockListTest__NamedBlock('a', false, 'block a'), + ], + 2, + ], + 'short block list with multiline block' => [ + <<<'STRING' + a: block a + + b: block b + STRING, + $settings, + 0, + 0, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__NamedBlock('a', false, 'block a'), + new BlockListTest__NamedBlock('b', true, 'block b'), + ], + 2, + ], + 'block list with multiline blocks' => [ + <<<'STRING' + a: block a + + b: block b + c: block c + + d: block d + + e: block e + f: block f + + g: block g + STRING, + $settings, + 0, + 0, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__NamedBlock('a', true, 'block a'), + new BlockListTest__NamedBlock('b', false, 'block b'), + new BlockListTest__NamedBlock('c', false, 'block c'), + new BlockListTest__NamedBlock('d', true, 'block d'), + new BlockListTest__NamedBlock('e', false, 'block e'), + new BlockListTest__NamedBlock('f', false, 'block f'), + new BlockListTest__NamedBlock('g', true, 'block g'), + ], + 7, + ], + 'block list with multiline blocks without wrap' => [ + <<<'STRING' + c: block c + b: block b + a: block a + STRING, + $settings, + 0, + 0, + false, + false, + '', + '', + ', ', + '', + [ + new BlockListTest__NamedBlock('c', true, 'block c'), + new BlockListTest__NamedBlock('b', false, 'block b'), + new BlockListTest__NamedBlock('a', true, 'block a'), + ], + 3, + ], + 'normalized block list' => [ + <<<'STRING' + a: block a, b: block b + STRING, + $settings, + 0, + 0, + true, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__NamedBlock('b', false, 'block b'), + new BlockListTest__NamedBlock('a', false, 'block a'), + ], + 2, + ], + 'multi-line with level' => [ + <<<'STRING' + a: block a + b: block b + STRING, + $settings->setIndent(' '), + 2, + 0, + false, + false, + '', + '', + ', ', + '', + [ + new BlockListTest__NamedBlock('a', true, 'block a'), + new BlockListTest__NamedBlock('b', true, 'block b'), + ], + 2, + ], + ]), + 'prefix & suffix' => new ArrayDataProvider([ + 'one single-line block' => [ + <<<'STRING' + [a: block a] + STRING, + $settings, + 0, + 0, + false, + true, + '[', + ']', + ', ', + '', + [ + new BlockListTest__NamedBlock('a', false, 'block a'), + ], + 1, + ], + 'one multi-line block' => [ + <<<'STRING' + [ + a: block a + ] + STRING, + $settings, + 0, + 0, + false, + true, + '[', + ']', + ', ', + '', + [ + new BlockListTest__NamedBlock('a', true, 'block a'), + ], + 1, + ], + 'short block list' => [ + <<<'STRING' + [block a, b: block b] + STRING, + $settings, + 0, + 0, + false, + true, + '[', + ']', + ', ', + '', + [ + new BlockListTest__Block(false, 'block a'), + new BlockListTest__NamedBlock('b', false, 'block b'), + ], + 2, + ], + 'long block list' => [ + <<<'STRING' + [ + b: block b + a: block a + ] + STRING, + $settings->setLineLength(20), + 0, + 5, + false, + true, + '[', + ']', + ', ', + '', + [ + new BlockListTest__NamedBlock('b', false, 'block b'), + new BlockListTest__NamedBlock('a', false, 'block a'), + ], + 2, + ], + 'short block list with multiline block' => [ + <<<'STRING' + [ + block a + + block b + ] + STRING, + $settings, + 0, + 0, + false, + true, + '[', + ']', + ', ', + '', + [ + new BlockListTest__Block(false, 'block a'), + new BlockListTest__Block(true, 'block b'), + ], + 2, + ], + 'multi-line with level' => [ + <<<'STRING' + [ + block a + ] + STRING, + $settings, + 2, + 0, + false, + false, + '[', + ']', + ', ', + '', + [ + new BlockListTest__Block(true, 'block a'), + ], + 1, + ], + 'empty' => [ + '', + $settings, + 0, + 0, + false, + false, + '[', + ']', + ', ', + '', + [], + 0, + ], + ]), + 'separators' => new ArrayDataProvider([ + 'single-line' => [ + <<<'STRING' + block a | block b + STRING, + $settings, + 0, + 0, + false, + true, + '', + '', + ' | ', + '||', + [ + new BlockListTest__Block(false, 'block a'), + new BlockListTest__Block(false, 'block b'), + ], + 2, + ], + 'multiline' => [ + <<<'STRING' + || block a + || block b + STRING, + $settings, + 0, + 120, + false, + true, + '', + '', + '|', + '|| ', + [ + new BlockListTest__Block(false, 'block a'), + new BlockListTest__Block(false, 'block b'), + ], + 2, + ], + 'multiline and indent' => [ + <<<'STRING' + || block a + || block b + STRING, + $settings, + 1, + 120, + false, + true, + '', + '', + '|', + '|| ', + [ + new BlockListTest__Block(false, 'block a'), + new BlockListTest__Block(false, 'block b'), + ], + 2, + ], + ]), + 'empty blocks' => new ArrayDataProvider([ + 'should be ignored' => [ + <<<'STRING' + block a + block b + STRING, + $settings, + 0, + 120, + false, + true, + '', + '', + ', ', + '', + [ + new BlockListTest__Block(false, ''), + new BlockListTest__Block(false, 'block a'), + new BlockListTest__Block(true, ''), + new BlockListTest__Block(false, 'block b'), + new BlockListTest__Block(false, ''), + ], + 2, + ], + ]), + ]))->getData(); + } + // +} + +// @phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses +// @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class BlockListTest__BlockList extends BlockList { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + private bool $normalized, + private bool $wrapped, + private string $prefix, + private string $suffix, + private string $separator, + private string $multilineSeparator, + ) { + parent::__construct($settings, $level, $used); + } + + protected function isWrapped(): bool { + return $this->wrapped; + } + + protected function isNormalized(): bool { + return $this->normalized; + } + + protected function getPrefix(): string { + return $this->prefix; + } + + protected function getSuffix(): string { + return $this->suffix; + } + + protected function getSeparator(): string { + return $this->separator; + } + + protected function getMultilineItemPrefix(): string { + return $this->multilineSeparator; + } +} + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class BlockListTest__Block extends Block { + /** @noinspection PhpMissingParentConstructorInspection */ + public function __construct( + protected bool $multiline, + protected string $content, + ) { + // empty + } + + protected function getContent(): string { + return $this->content; + } + + public function getLength(): int { + return mb_strlen($this->getContent()); + } + + public function isMultiline(): bool { + return $this->multiline; + } + + protected function content(): string { + return ''; + } +} + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class BlockListTest__NamedBlock extends Property { + /** @noinspection PhpMissingParentConstructorInspection */ + public function __construct( + protected string $name, + protected bool $multiline, + protected string $content, + ) { + // empty + } + + public function getName(): string { + return $this->name; + } + + protected function getBlock(): Block { + return new BlockListTest__Block($this->multiline, $this->content); + } + + protected function space(): string { + return ' '; + } +} + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class BlockListTest__StatisticsBlock extends Block { + /** + * @param array $types + * @param array $directives + * + * @noinspection PhpMissingParentConstructorInspection + */ + public function __construct(array $types, array $directives) { + foreach ($types as $type) { + $this->addUsedType($type); + } + + foreach ($directives as $directive) { + $this->addUsedDirective($directive); + } + } + + public function isEmpty(): bool { + return false; + } + + protected function content(): string { + return ''; + } +} diff --git a/src/SchemaPrinter/Blocks/BlockTest.php b/src/SchemaPrinter/Blocks/BlockTest.php new file mode 100644 index 00000000..8640a88f --- /dev/null +++ b/src/SchemaPrinter/Blocks/BlockTest.php @@ -0,0 +1,161 @@ + + // ========================================================================= + /** + * @covers ::getContent + */ + public function testGetContent(): void { + $settings = new TestSettings(); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $content = 'content'; + $block = Mockery::mock(BlockTest__Block::class, [$settings]); + $block->shouldAllowMockingProtectedMethods(); + $block->makePartial(); + $block + ->shouldReceive('content') + ->once() + ->andReturn($content); + + self::assertEquals($content, $block->getContent()); + self::assertEquals($content, $block->getContent()); + } + + /** + * @covers ::getLength + */ + public function testGetLength(): void { + $settings = new TestSettings(); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $content = 'content'; + $length = mb_strlen($content); + $block = Mockery::mock(BlockTest__Block::class, [$settings]); + $block->shouldAllowMockingProtectedMethods(); + $block->makePartial(); + $block + ->shouldReceive('content') + ->once() + ->andReturn($content); + + self::assertEquals($length, $block->getLength()); + self::assertEquals($length, $block->getLength()); + } + + /** + * @covers ::isMultiline + * + * @dataProvider dataProviderIsMultiline + */ + public function testIsMultiline(bool $expected, Settings $settings, string $content): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $block = Mockery::mock(BlockTest__Block::class, [$settings]); + $block->shouldAllowMockingProtectedMethods(); + $block->makePartial(); + $block + ->shouldReceive('content') + ->once() + ->andReturn($content); + + self::assertEquals($expected, $block->isMultiline()); + self::assertEquals($expected, $block->isMultiline()); + } + + /** + * @covers ::isEmpty + * + * @dataProvider dataProviderIsEmpty + */ + public function testIsEmpty(bool $expected, string $content): void { + $settings = new TestSettings(); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $block = Mockery::mock(BlockTest__Block::class, [$settings]); + $block->shouldAllowMockingProtectedMethods(); + $block->makePartial(); + $block + ->shouldReceive('content') + ->once() + ->andReturn($content); + + self::assertEquals($expected, $block->isEmpty()); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderIsMultiline(): array { + $settings = new TestSettings(); + + return [ + 'single short line' => [ + false, + $settings, + 'short line', + ], + 'single long line' => [ + false, + $settings->setLineLength(5), + 'long line', + ], + 'multi line' => [ + true, + $settings, + "multi\nline", + ], + ]; + } + + /** + * @return array + */ + public function dataProviderIsEmpty(): array { + return [ + 'empty' => [true, ''], + 'non empty' => [false, 'content'], + ]; + } + // +} + +// @phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses +// @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class BlockTest__Block extends Block { + public function getContent(): string { + return parent::getContent(); + } + + public function getLength(): int { + return parent::getLength(); + } + + public function isMultiline(): bool { + return parent::isMultiline(); + } + + protected function content(): string { + return ''; + } +} diff --git a/src/SchemaPrinter/Blocks/Named.php b/src/SchemaPrinter/Blocks/Named.php new file mode 100644 index 00000000..de2b0193 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Named.php @@ -0,0 +1,10 @@ + + */ +class ObjectBlockList extends BlockList { + protected function getPrefix(): string { + return '{'; + } + + protected function getSuffix(): string { + return '}'; + } + + protected function isAlwaysMultiline(): bool { + return true; + } +} diff --git a/src/SchemaPrinter/Blocks/Printer/DefinitionBlock.php b/src/SchemaPrinter/Blocks/Printer/DefinitionBlock.php new file mode 100644 index 00000000..07f479a8 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Printer/DefinitionBlock.php @@ -0,0 +1,127 @@ +block = $this->getDefinitionBlock($definition); + } + + public function getName(): string { + $name = ''; + $block = $this->getBlock(); + + if ($block instanceof Named) { + $name = $block->getName(); + } + + return $name; + } + + protected function getBlock(): Block { + return $this->block; + } + + protected function content(): string { + return (string) $this->addUsed($this->getBlock()); + } + + protected function getDefinitionBlock(Schema|Type|Directive $definition): Block { + $block = null; + + if ($definition instanceof ObjectType) { + $block = new ObjectTypeDefinitionBlock( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $definition, + ); + } elseif ($definition instanceof InputObjectType) { + $block = new InputObjectTypeDefinitionBlock( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $definition, + ); + } elseif ($definition instanceof ScalarType) { + $block = new ScalarTypeDefinitionBlock( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $definition, + ); + } elseif ($definition instanceof InterfaceType) { + $block = new InterfaceTypeDefinitionBlock( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $definition, + ); + } elseif ($definition instanceof UnionType) { + $block = new UnionTypeDefinitionBlock( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $definition, + ); + } elseif ($definition instanceof EnumType) { + $block = new EnumTypeDefinitionBlock( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $definition, + ); + } elseif ($definition instanceof Directive) { + $block = new DirectiveDefinitionBlock( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $definition, + ); + } elseif ($definition instanceof Schema) { + $block = new SchemaDefinitionBlock( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $definition, + ); + } else { + throw new TypeUnsupported($definition); + } + + return $block; + } +} diff --git a/src/SchemaPrinter/Blocks/Printer/DefinitionList.php b/src/SchemaPrinter/Blocks/Printer/DefinitionList.php new file mode 100644 index 00000000..ab5b697c --- /dev/null +++ b/src/SchemaPrinter/Blocks/Printer/DefinitionList.php @@ -0,0 +1,51 @@ + + */ +class DefinitionList extends BlockList { + public function __construct( + PrinterSettings $settings, + int $level, + protected bool $schema = false, + ) { + parent::__construct($settings, $level); + } + + protected function isWrapped(): bool { + return true; + } + + protected function isNormalized(): bool { + return $this->getSettings()->isNormalizeSchema(); + } + + protected function isAlwaysMultiline(): bool { + return true; + } + + protected function isSchema(): bool { + return $this->schema; + } + + protected function content(): string { + $content = parent::content(); + + if ($content && $this->isSchema()) { + $eof = $this->getSettings()->getFileEnd(); + $content = rtrim($content); + $content = "{$this->indent()}{$content}{$eof}"; + } + + return $content; + } +} diff --git a/src/SchemaPrinter/Blocks/Property.php b/src/SchemaPrinter/Blocks/Property.php new file mode 100644 index 00000000..936b9931 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Property.php @@ -0,0 +1,50 @@ +getLevel(), $block->getUsed()); + } + + public function getName(): string { + return $this->name; + } + + public function isMultiline(): bool { + return $this->getBlock()->isMultiline() || parent::isMultiline(); + } + + /** + * @return TBlock + */ + protected function getBlock(): Block { + return $this->block; + } + + protected function getSeparator(): string { + return ':'; + } + + protected function content(): string { + $block = $this->addUsed($this->getBlock()); + $content = "{$this->getName()}{$this->getSeparator()}{$this->space()}{$block}"; + + return $content; + } +} diff --git a/src/SchemaPrinter/Blocks/PropertyTest.php b/src/SchemaPrinter/Blocks/PropertyTest.php new file mode 100644 index 00000000..8f734430 --- /dev/null +++ b/src/SchemaPrinter/Blocks/PropertyTest.php @@ -0,0 +1,76 @@ +setSpace($space); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $block = new class($settings, $level, $used, $content) extends Block { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + protected string $content, + ) { + parent::__construct($settings, $level, $used); + } + + protected function content(): string { + return $this->content; + } + }; + $property = new class($settings, $name, $block, $separator) extends Property { + public function __construct( + PrinterSettings $settings, + string $name, + Block $block, + private string $separator, + ) { + parent::__construct($settings, $name, $block); + } + + public function getUsed(): int { + return parent::getUsed(); + } + + public function getLevel(): int { + return parent::getLevel(); + } + + protected function getSeparator(): string { + return $this->separator; + } + }; + $expected = "{$name}{$separator}{$space}{$content}"; + + self::assertEquals($used, $property->getUsed()); + self::assertEquals($level, $property->getLevel()); + self::assertEquals($expected, (string) $property); + self::assertEquals(mb_strlen($expected), mb_strlen((string) $property)); + self::assertEquals(mb_strlen($expected), $property->getLength()); + } +} diff --git a/src/SchemaPrinter/Blocks/Types/ArgumentsDefinitionList.php b/src/SchemaPrinter/Blocks/Types/ArgumentsDefinitionList.php new file mode 100644 index 00000000..9161684f --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/ArgumentsDefinitionList.php @@ -0,0 +1,51 @@ + + */ +class ArgumentsDefinitionList extends BlockList { + /** + * @param Traversable|array $arguments + */ + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + Traversable|array $arguments, + ) { + parent::__construct($settings, $level, $used); + + foreach ($arguments as $argument) { + $this[$argument->name] = new InputValueDefinitionBlock( + $this->getSettings(), + $this->getLevel() + 1, + $this->getUsed(), + $argument, + ); + } + } + + protected function getPrefix(): string { + return '('; + } + + protected function getSuffix(): string { + return ')'; + } + + protected function isWrapped(): bool { + return true; + } + + protected function isNormalized(): bool { + return $this->getSettings()->isNormalizeArguments(); + } +} diff --git a/src/SchemaPrinter/Blocks/Types/DefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/DefinitionBlock.php new file mode 100644 index 00000000..ca7e6f27 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/DefinitionBlock.php @@ -0,0 +1,167 @@ +name(); + $type = $this->type(); + + if ($type && $name) { + $space = $this->space(); + $name = "{$type}{$space}{$name}"; + } elseif ($type) { + $name = $type; + } else { + // empty + } + + return $name; + } + + /** + * @return TType + */ + protected function getDefinition( + // empty + ): Type|FieldDefinition|EnumValueDefinition|FieldArgument|Directive|InputObjectField|Schema { + return $this->definition; + } + + protected function content(): string { + $eol = $this->eol(); + $space = $this->space(); + $indent = $this->indent(); + $name = $this->getName(); + $used = $this->getUsed() + mb_strlen($name) + mb_strlen($space); + $body = (string) $this->addUsed($this->body($used)); + $fields = (string) $this->addUsed($this->fields($used + mb_strlen($body))); + $directives = $this->directives(); + $description = (string) $this->addUsed($this->description($directives)); + $directives = $this->getSettings()->isPrintDirectives() + ? (string) $this->addUsed($directives) + : ''; + $content = ''; + + if ($description) { + $content .= "{$description}{$eol}{$indent}"; + } + + $content .= "{$name}{$body}"; + + if ($directives) { + $content .= "{$eol}{$indent}{$directives}"; + } + + if ($fields) { + if ((bool) $directives || $this->isStringMultiline($body)) { + $content .= "{$eol}{$indent}{$fields}"; + } else { + $content .= "{$space}{$fields}"; + } + } + + return $content; + } + + protected function name(): string { + $definition = $this->getDefinition(); + $name = !($definition instanceof Schema) + ? $definition->name + : ''; + + return $name; + } + + abstract protected function type(): string|null; + + abstract protected function body(int $used): Block|string|null; + + abstract protected function fields(int $used): Block|string|null; + + protected function directives(): DirectiveNodeList { + $definition = $this->getDefinition(); + $directives = new DirectiveNodeList( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $this->getDefinitionDirectives(), + $definition->deprecationReason ?? null, + ); + + return $directives; + } + + protected function description(DirectiveNodeList $directives): ?Description { + // Supported? + $definition = $this->getDefinition(); + $description = null; + + if ($definition instanceof Schema) { + // It is part of October2021 spec but not yet supported + // https://github.com/webonyx/graphql-php/issues/1027 + } else { + $description = $definition->description; + } + + return new Description( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $description, + $directives, + ); + } + + /** + * @return NodeList + */ + protected function getDefinitionDirectives(): NodeList { + $definition = $this->getDefinition(); + $astNode = $definition instanceof Schema + ? $definition->getAstNode() + : $definition->astNode; + $directives = $astNode?->directives ?? null; + + if ($directives === null) { + /** @var NodeList $empty */ + $empty = new NodeList([]); + $directives = $empty; + } + + return $directives; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/Description.php b/src/SchemaPrinter/Blocks/Types/Description.php new file mode 100644 index 00000000..2382a15e --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/Description.php @@ -0,0 +1,74 @@ +directives; + } + + protected function isNormalized(): bool { + return $this->getSettings()->isNormalizeDescription(); + } + + protected function isBlock(): bool { + return true; + } + + protected function getString(): string { + // Normalize + $string = parent::getString(); + + if ($this->isNormalized()) { + $eol = $this->eol(); + $string = str_replace(["\r\n", "\n\r", "\n", "\r"], $eol, $string); + $string = rtrim(trim($string, $eol)); + $string = (string) preg_replace('/\R{2,}/u', "{$eol}{$eol}", $string); + $string = (string) preg_replace('/^(.*?)\h+$/mu', '$1', $string); + } + + // Directives + if ($this->getSettings()->isPrintDirectivesInDescription()) { + $directives = (string) $this->getDirectives(); + + if ($directives) { + $eol = $string ? $this->eol() : ''; + $string = "{$string}{$eol}{$eol}{$directives}"; + } + } + + // Return + return $string; + } + + protected function content(): string { + $content = parent::content(); + + if ($content === '""""""') { + $content = ''; + } + + return $content; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/DescriptionTest.php b/src/SchemaPrinter/Blocks/Types/DescriptionTest.php new file mode 100644 index 00000000..7a0616f6 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/DescriptionTest.php @@ -0,0 +1,414 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + * + * @param array|null $directives + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + ?string $description, + ?array $directives, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $directives = new DirectiveNodeList($settings, $level, $used, $directives); + $actual = (string) (new Description($settings, $level, $used, $description, $directives)); + + self::assertEquals($expected, $actual); + + if ($expected) { + Parser::valueLiteral($actual); + } + } + // + + // + // ========================================================================= + /** + * @return array|null}> + */ + public function dataProviderToString(): array { + $settings = (new TestSettings()) + ->setNormalizeDescription(false); + + return [ + 'null' => [ + '', + $settings, + 0, + 0, + null, + null, + ], + 'Prints an empty string' => [ + '', + $settings, + 0, + 0, + '', + [], + ], + 'Prints an empty string (normalized)' => [ + '', + $settings->setNormalizeDescription(true), + 0, + 0, + '', + [], + ], + 'Prints an empty string with only whitespace' => [ + '" "', + $settings, + 0, + 0, + ' ', + [], + ], + 'Prints an empty string with only whitespace (normalized)' => [ + '', + $settings->setNormalizeDescription(true), + 0, + 0, + ' ', + [], + ], + 'One-line prints a short string' => [ + <<<'STRING' + """ + Short string + """ + STRING, + $settings, + 0, + 0, + 'Short string', + [], + ], + 'One-line prints a long string' => [ + <<<'STRING' + """ + Long string + """ + STRING, + $settings->setLineLength(4), + 0, + 0, + 'Long string', + [], + ], + 'String is short (indent)' => [ + <<<'STRING' + """ + string + """ + STRING, + $settings + ->setIndent(' ') + ->setLineLength(2), + 2, + 0, + 'string', + [], + ], + 'String is long (indent)' => [ + <<<'STRING' + """ + string + """ + STRING, + $settings + ->setIndent(' ') + ->setLineLength(22), + 2, + 20, + 'string', + [], + ], + 'Multi-line string' => [ + <<<'STRING' + """ + aaa + bbb + + + + ccc + """ + STRING, + $settings, + 0, + 0, + <<<'STRING' + aaa + bbb + + + + ccc + STRING, + [], + ], + 'Multi-line string (normalized)' => [ + <<<'STRING' + """ + aaa + bbb + + ccc + """ + STRING, + $settings->setNormalizeDescription(true), + 0, + 0, + <<<'STRING' + aaa + bbb + + + ccc + STRING, + [], + ], + 'Leading space' => [ + <<<'STRING' + """ Leading space""" + STRING, + $settings, + 0, + 0, + ' Leading space', + [], + ], + 'Leading tab' => [ + "\"\"\"\tLeading tab\"\"\"", + $settings, + 0, + 0, + "\tLeading tab", + [], + ], + 'Trailing "' => [ + <<<'STRING' + """ + Trailing " + """ + STRING, + $settings, + 0, + 0, + 'Trailing "', + [], + ], + 'Leading whitespace and trailing "' => [ + <<<'STRING' + """ + Leading whitespace and trailing " + abc + """ + STRING, + $settings, + 0, + 0, + <<<'STRING' + Leading whitespace and trailing " + abc + STRING, + [], + ], + 'Trailing backslash' => [ + <<<'STRING' + """ + Trailing \\ + """ + STRING, + $settings, + 0, + 0, + 'Trailing \\\\', + [], + ], + 'Escape wrapper' => [ + <<<'STRING' + """ + String with \""" wrapper + """ + STRING, + $settings, + 0, + 0, + 'String with """ wrapper', + [], + ], + 'Indent' => [ + implode( + "\n", + [ + '"""', + ' aaa', + '', + ' bbb ', + ' ccc ', + ' ', + ' ddd ', + ' """', + ], + ), + $settings->setIndent(' '), + 2, + 0, + implode( + "\n", + [ + ' aaa', + '', + ' bbb ', + 'ccc ', + ' ', + ' ddd ', + ], + ), + [], + ], + 'Indent (normalized)' => [ + implode( + "\n", + [ + '"""', + ' aaa', + '', + ' bbb', + ' ccc', + '', + ' ddd', + ' """', + ], + ), + $settings + ->setIndent(' ') + ->setNormalizeDescription(true), + 2, + 0, + implode( + "\n", + [ + ' aaa', + '', + ' bbb ', + 'ccc ', + ' ', + ' ddd ', + ], + ), + [], + ], + 'directives (disabled)' => [ + <<<'STRING' + """ + Description + """ + STRING, + $settings, + 0, + 0, + <<<'STRING' + Description + STRING, + [ + Parser::directive('@a'), + ], + ], + 'directives (enabled)' => [ + <<<'STRING' + """ + Description + + + + @a + @b(test: "abc") + """ + STRING, + $settings->setPrintDirectivesInDescription(true), + 0, + 0, + <<<'STRING' + Description + + + STRING, + [ + Parser::directive('@a'), + Parser::directive('@b(test: "abc")'), + ], + ], + 'directives (enabled) + normalized' => [ + <<<'STRING' + """ + Description + + @a + @b(test: "abc") + """ + STRING, + $settings + ->setNormalizeDescription(true) + ->setPrintDirectivesInDescription(true), + 0, + 0, + <<<'STRING' + Description + + + STRING, + [ + Parser::directive('@a'), + Parser::directive('@b(test: "abc")'), + ], + ], + 'empty description + directives (enabled) + normalized' => [ + <<<'STRING' + """ + @a + @b(test: "abc") + """ + STRING, + $settings + ->setNormalizeDescription(true) + ->setPrintDirectivesInDescription(true), + 0, + 0, + '', + [ + Parser::directive('@a'), + Parser::directive('@b(test: "abc")'), + ], + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/DirectiveDefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/DirectiveDefinitionBlock.php new file mode 100644 index 00000000..6feb0d3b --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/DirectiveDefinitionBlock.php @@ -0,0 +1,83 @@ + + */ +class DirectiveDefinitionBlock extends DefinitionBlock { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + Directive $definition, + ) { + parent::__construct($settings, $level, $used, $definition); + } + + protected function type(): string|null { + return 'directive'; + } + + protected function name(): string { + return '@'.parent::name(); + } + + protected function body(int $used): Block|string|null { + $definition = $this->getDefinition(); + $eol = $this->eol(); + $space = $this->space(); + $indent = $this->indent(); + $repeatable = 'repeatable'; + $used = $used + mb_strlen($repeatable) + 2 * mb_strlen($space); + $args = $this->addUsed( + new ArgumentsDefinitionList( + $this->getSettings(), + $this->getLevel(), + $used, + $definition->args, + ), + ); + $locations = $this->addUsed( + new DirectiveLocationsList( + $this->getSettings(), + $this->getLevel() + 1, + $used + $args->getLength(), + $definition->locations, + ), + ); + $content = "{$args}"; + + if ($args->isMultiline()) { + $content .= "{$eol}{$indent}"; + } + + if ($definition->isRepeatable) { + if (!$args->isMultiline()) { + $content .= "{$space}"; + } + + $content .= "{$repeatable}{$space}{$locations}"; + } else { + if (!$args->isMultiline()) { + $content .= "{$space}"; + } + + $content .= "{$locations}"; + } + + return $content; + } + + protected function fields(int $used): Block|string|null { + return null; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/DirectiveDefinitionBlockTest.php b/src/SchemaPrinter/Blocks/Types/DirectiveDefinitionBlockTest.php new file mode 100644 index 00000000..a9abbff0 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/DirectiveDefinitionBlockTest.php @@ -0,0 +1,263 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + Directive $definition, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new DirectiveDefinitionBlock($settings, $level, $used, $definition)); + + Parser::directiveDefinition($actual); + + self::assertEquals($expected, $actual); + } + + /** + * @covers ::__toString + */ + public function testStatistics(): void { + $settings = new TestSettings(); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $definition = new Directive([ + 'name' => 'A', + 'args' => [ + 'a' => [ + 'type' => new ObjectType(['name' => 'B']), + ], + ], + 'locations' => [ + DirectiveLocation::FIELD, + ], + ]); + $block = new DirectiveDefinitionBlock($settings, 0, 0, $definition); + + self::assertNotEmpty((string) $block); + self::assertEquals(['B' => 'B'], $block->getUsedTypes()); + self::assertEquals([], $block->getUsedDirectives()); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = (new TestSettings()) + ->setAlwaysMultilineDirectiveLocations(false); + + return [ + 'description' => [ + <<<'STRING' + """ + Description + """ + directive @test on ARGUMENT_DEFINITION | ENUM + STRING, + $settings, + 0, + 0, + new Directive([ + 'name' => 'test', + 'description' => 'Description', + 'locations' => [ + DirectiveLocation::ARGUMENT_DEFINITION, + DirectiveLocation::ENUM, + ], + ]), + ], + 'repeatable' => [ + <<<'STRING' + directive @test repeatable on ARGUMENT_DEFINITION | ENUM + STRING, + $settings, + 0, + 0, + new Directive([ + 'name' => 'test', + 'locations' => [ + DirectiveLocation::ARGUMENT_DEFINITION, + DirectiveLocation::ENUM, + ], + 'isRepeatable' => true, + ]), + ], + 'args' => [ + <<<'STRING' + directive @test(a: String) repeatable on ARGUMENT_DEFINITION | ENUM + STRING, + $settings, + 0, + 0, + new Directive([ + 'name' => 'test', + 'args' => [ + 'a' => [ + 'type' => Type::string(), + ], + ], + 'locations' => [ + DirectiveLocation::ARGUMENT_DEFINITION, + DirectiveLocation::ENUM, + ], + 'isRepeatable' => true, + ]), + ], + 'multiline + repeatable' => [ + <<<'STRING' + directive @test( + a: String + ) + repeatable on + | ARGUMENT_DEFINITION + | ENUM + STRING, + $settings, + 0, + 120, + new Directive([ + 'name' => 'test', + 'args' => [ + 'a' => [ + 'type' => Type::string(), + ], + ], + 'locations' => [ + DirectiveLocation::ARGUMENT_DEFINITION, + DirectiveLocation::ENUM, + ], + 'isRepeatable' => true, + ]), + ], + 'multiline' => [ + <<<'STRING' + directive @test( + a: String + ) + on + | ARGUMENT_DEFINITION + | ENUM + STRING, + $settings, + 0, + 120, + new Directive([ + 'name' => 'test', + 'args' => [ + 'a' => [ + 'type' => Type::string(), + ], + ], + 'locations' => [ + DirectiveLocation::ARGUMENT_DEFINITION, + DirectiveLocation::ENUM, + ], + ]), + ], + 'multiline (no args)' => [ + <<<'STRING' + directive @test on + | ARGUMENT_DEFINITION + | ENUM + STRING, + $settings, + 0, + 60, + new Directive([ + 'name' => 'test', + 'locations' => [ + DirectiveLocation::ARGUMENT_DEFINITION, + DirectiveLocation::ENUM, + ], + ]), + ], + 'indent' => [ + <<<'STRING' + directive @test( + a: String + ) + on + | ARGUMENT_DEFINITION + | ENUM + STRING, + $settings, + 1, + 120, + new Directive([ + 'name' => 'test', + 'args' => [ + 'a' => [ + 'type' => Type::string(), + ], + ], + 'locations' => [ + DirectiveLocation::ARGUMENT_DEFINITION, + DirectiveLocation::ENUM, + ], + ]), + ], + 'normalized' => [ + <<<'STRING' + directive @test on ENUM | INPUT_FIELD_DEFINITION | OBJECT + STRING, + $settings + ->setNormalizeDirectiveLocations(true), + 0, + 0, + new Directive([ + 'name' => 'test', + 'locations' => [ + DirectiveLocation::OBJECT, + DirectiveLocation::ENUM, + DirectiveLocation::INPUT_FIELD_DEFINITION, + ], + ]), + ], + 'locations always multiline' => [ + <<<'STRING' + directive @test on + | ARGUMENT_DEFINITION + STRING, + $settings + ->setAlwaysMultilineDirectiveLocations(true), + 0, + 0, + new Directive([ + 'name' => 'test', + 'locations' => [ + DirectiveLocation::ARGUMENT_DEFINITION, + ], + ]), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/DirectiveLocationBlock.php b/src/SchemaPrinter/Blocks/Types/DirectiveLocationBlock.php new file mode 100644 index 00000000..a1a4367f --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/DirectiveLocationBlock.php @@ -0,0 +1,33 @@ +getLocation(); + } + + protected function getLocation(): string { + return $this->location; + } + + protected function content(): string { + return $this->getLocation(); + } +} diff --git a/src/SchemaPrinter/Blocks/Types/DirectiveLocationsList.php b/src/SchemaPrinter/Blocks/Types/DirectiveLocationsList.php new file mode 100644 index 00000000..d0b4d5cf --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/DirectiveLocationsList.php @@ -0,0 +1,36 @@ + + */ +class DirectiveLocationsList extends UsageList { + protected function block(mixed $item): Block { + return new DirectiveLocationBlock( + $this->getSettings(), + $this->getLevel() + 1, + $this->getUsed(), + $item, + ); + } + + protected function separator(): string { + return '|'; + } + + protected function prefix(): string { + return 'on'; + } + + protected function isNormalized(): bool { + return $this->getSettings()->isNormalizeDirectiveLocations(); + } + + protected function isAlwaysMultiline(): bool { + return $this->getSettings()->isAlwaysMultilineDirectiveLocations(); + } +} diff --git a/src/SchemaPrinter/Blocks/Types/EnumTypeDefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/EnumTypeDefinitionBlock.php new file mode 100644 index 00000000..191b0552 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/EnumTypeDefinitionBlock.php @@ -0,0 +1,47 @@ + + */ +class EnumTypeDefinitionBlock extends DefinitionBlock { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + EnumType $definition, + ) { + parent::__construct($settings, $level, $used, $definition); + } + + protected function type(): string { + return 'enum'; + } + + protected function body(int $used): Block|string|null { + $space = $this->space(); + $values = $this->addUsed( + new EnumValuesDefinitionList( + $this->getSettings(), + $this->getLevel(), + $used + mb_strlen($space), + $this->getDefinition()->getValues(), + ), + ); + + return "{$space}{$values}"; + } + + protected function fields(int $used): Block|string|null { + return null; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/EnumTypeDefinitionBlockTest.php b/src/SchemaPrinter/Blocks/Types/EnumTypeDefinitionBlockTest.php new file mode 100644 index 00000000..9755556c --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/EnumTypeDefinitionBlockTest.php @@ -0,0 +1,151 @@ + + // ========================================================================= + /** + * @covers ::__toString + * @covers \LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Types\EnumValuesDefinitionList::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + Closure|EnumType $type, + ): void { + if ($type instanceof Closure) { + $type = $type(); + } + + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new EnumTypeDefinitionBlock($settings, $level, $used, $type)); + + Parser::enumTypeDefinition($actual); + + self::assertEquals($expected, $actual); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = (new TestSettings()) + ->setNormalizeEnums(false); + + return [ + 'enum' => [ + <<<'STRING' + enum Test { + C + B + A + } + STRING, + $settings, + 0, + 0, + new EnumType([ + 'name' => 'Test', + 'values' => ['C', 'B', 'A'], + ]), + ], + 'indent' => [ + <<<'STRING' + enum Test { + C + B + A + } + STRING, + $settings, + 1, + 0, + new EnumType([ + 'name' => 'Test', + 'values' => ['C', 'B', 'A'], + ]), + ], + 'normalized' => [ + <<<'STRING' + enum Test { + A + B + C + } + STRING, + $settings->setNormalizeEnums(true), + 0, + 0, + new EnumType([ + 'name' => 'Test', + 'values' => ['C', 'B', 'A'], + ]), + ], + 'directives and description' => [ + <<<'STRING' + enum Test { + C + + """ + Description + """ + B + @b + @a + + A + @deprecated + } + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + static function (): EnumType { + $enum = new EnumType([ + 'name' => 'Test', + 'values' => ['C', 'B', 'A'], + ]); + + $a = $enum->getValue('A'); + + if ($a instanceof EnumValueDefinition) { + $a->deprecationReason = Directive::DEFAULT_DEPRECATION_REASON; + } + + $b = $enum->getValue('B'); + + if ($b instanceof EnumValueDefinition) { + $b->astNode = Parser::enumValueDefinition('A @b @a'); + $b->description = 'Description'; + } + + return $enum; + }, + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/EnumValueDefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/EnumValueDefinitionBlock.php new file mode 100644 index 00000000..a84a0398 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/EnumValueDefinitionBlock.php @@ -0,0 +1,34 @@ + + */ +class EnumValueDefinitionBlock extends DefinitionBlock { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + EnumValueDefinition $value, + ) { + parent::__construct($settings, $level, $used, $value); + } + + protected function type(): string|null { + return null; + } + + protected function body(int $used): Block|string|null { + return null; + } + + protected function fields(int $used): Block|string|null { + return null; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/EnumValueDefinitionBlockTest.php b/src/SchemaPrinter/Blocks/Types/EnumValueDefinitionBlockTest.php new file mode 100644 index 00000000..83d0d8b6 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/EnumValueDefinitionBlockTest.php @@ -0,0 +1,95 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + EnumValueDefinition $type, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new EnumValueDefinitionBlock($settings, $level, $used, $type)); + + Parser::enumValueDefinition($actual); + + self::assertEquals($expected, $actual); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = new TestSettings(); + + return [ + 'value' => [ + <<<'STRING' + A + STRING, + $settings, + 0, + 0, + new EnumValueDefinition([ + 'name' => 'A', + 'value' => 'A', + ]), + ], + 'indent' => [ + <<<'STRING' + A + STRING, + $settings, + 1, + 0, + new EnumValueDefinition([ + 'name' => 'A', + 'value' => 'A', + ]), + ], + 'description and directives' => [ + <<<'STRING' + """ + Description + """ + A + @a + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + new EnumValueDefinition([ + 'name' => 'A', + 'value' => 'A', + 'astNode' => Parser::enumValueDefinition('A @a'), + 'description' => 'Description', + ]), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/EnumValuesDefinitionList.php b/src/SchemaPrinter/Blocks/Types/EnumValuesDefinitionList.php new file mode 100644 index 00000000..a620aaf1 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/EnumValuesDefinitionList.php @@ -0,0 +1,47 @@ + + */ +class EnumValuesDefinitionList extends ObjectBlockList { + /** + * @param Traversable|array $values + */ + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + Traversable|array $values, + ) { + parent::__construct($settings, $level, $used); + + foreach ($values as $value) { + $this[$value->name] = new EnumValueDefinitionBlock( + $this->getSettings(), + $this->getLevel() + 1, + $this->getUsed(), + $value, + ); + } + } + + protected function isWrapped(): bool { + return true; + } + + protected function isNormalized(): bool { + return $this->getSettings()->isNormalizeEnums(); + } + + protected function isAlwaysMultiline(): bool { + return true; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/FieldDefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/FieldDefinitionBlock.php new file mode 100644 index 00000000..5e9f3653 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/FieldDefinitionBlock.php @@ -0,0 +1,54 @@ + + */ +class FieldDefinitionBlock extends DefinitionBlock { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + FieldDefinition $definition, + ) { + parent::__construct($settings, $level, $used, $definition); + } + + protected function type(): string|null { + return null; + } + + protected function body(int $used): Block|string|null { + $definition = $this->getDefinition(); + $space = $this->space(); + $type = $this->addUsed( + new TypeBlock( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $definition->getType(), + ), + ); + $args = $this->addUsed( + new ArgumentsDefinitionList( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $definition->args, + ), + ); + + return "{$args}:{$space}{$type}"; + } + + protected function fields(int $used): Block|string|null { + return null; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/FieldDefinitionBlockTest.php b/src/SchemaPrinter/Blocks/Types/FieldDefinitionBlockTest.php new file mode 100644 index 00000000..9f29ac1c --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/FieldDefinitionBlockTest.php @@ -0,0 +1,217 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + FieldDefinition $definition, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new FieldDefinitionBlock($settings, $level, $used, $definition)); + + Parser::fieldDefinition($actual); + + self::assertEquals($expected, $actual); + } + + /** + * @covers ::__toString + */ + public function testStatistics(): void { + $settings = new TestSettings(); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $definition = FieldDefinition::create([ + 'name' => 'A', + 'type' => new NonNull( + new ObjectType([ + 'name' => 'A', + ]), + ), + 'astNode' => Parser::fieldDefinition('a: A @a'), + ]); + $block = new FieldDefinitionBlock($settings, 0, 0, $definition); + + self::assertNotEmpty((string) $block); + self::assertEquals(['A' => 'A'], $block->getUsedTypes()); + self::assertEquals(['@a' => '@a'], $block->getUsedDirectives()); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = (new TestSettings()) + ->setNormalizeArguments(false); + + return [ + 'without args' => [ + <<<'STRING' + """ + Description + """ + test: Test! + @a + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + FieldDefinition::create([ + 'name' => 'test', + 'type' => new NonNull( + new ObjectType([ + 'name' => 'Test', + ]), + ), + 'astNode' => Parser::fieldDefinition('test: Test! @a'), + 'description' => 'Description', + ]), + ], + 'with args (short)' => [ + <<<'STRING' + """ + Description + """ + test(a: [String!] = ["aaaaaaaaaaaaaaaaaaaaaaaaaa"], b: Int): Test! + STRING, + $settings, + 0, + 0, + FieldDefinition::create([ + 'name' => 'test', + 'type' => new NonNull( + new ObjectType([ + 'name' => 'Test', + ]), + ), + 'args' => [ + 'a' => [ + 'type' => new ListOfType(new NonNull(Type::string())), + 'defaultValue' => [ + 'aaaaaaaaaaaaaaaaaaaaaaaaaa', + ], + ], + 'b' => [ + 'type' => Type::int(), + ], + ], + 'description' => 'Description', + ]), + ], + 'with args (long)' => [ + <<<'STRING' + test( + b: Int + + """ + Description + """ + a: String! = "aaaaaaaaaaaaaaaaaaaaaaaaaa" + ): Test! + STRING, + $settings, + 0, + 0, + FieldDefinition::create([ + 'name' => 'test', + 'type' => new NonNull( + new ObjectType([ + 'name' => 'Test', + ]), + ), + 'args' => [ + 'b' => [ + 'type' => Type::int(), + ], + 'a' => [ + 'type' => new NonNull(Type::string()), + 'description' => 'Description', + 'defaultValue' => 'aaaaaaaaaaaaaaaaaaaaaaaaaa', + ], + ], + ]), + ], + 'with args normalized' => [ + <<<'STRING' + test(a: String, b: Int): Test! + STRING, + $settings->setNormalizeArguments(true), + 0, + 0, + FieldDefinition::create([ + 'name' => 'test', + 'type' => new NonNull( + new ObjectType([ + 'name' => 'Test', + ]), + ), + 'args' => [ + 'b' => [ + 'type' => Type::int(), + ], + 'a' => [ + 'type' => Type::string(), + ], + ], + ]), + ], + 'indent' => [ + <<<'STRING' + test( + a: String + b: Int + ): Test! + STRING, + $settings->setNormalizeArguments(true), + 1, + 120, + FieldDefinition::create([ + 'name' => 'test', + 'type' => new NonNull( + new ObjectType([ + 'name' => 'Test', + ]), + ), + 'args' => [ + 'b' => [ + 'type' => Type::int(), + ], + 'a' => [ + 'type' => Type::string(), + ], + ], + ]), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/FieldsDefinitionList.php b/src/SchemaPrinter/Blocks/Types/FieldsDefinitionList.php new file mode 100644 index 00000000..c7c1b66e --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/FieldsDefinitionList.php @@ -0,0 +1,55 @@ + + */ +class FieldsDefinitionList extends BlockList { + /** + * @param Traversable|array $fields + */ + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + Traversable|array $fields, + ) { + parent::__construct($settings, $level, $used); + + foreach ($fields as $field) { + $this[$field->name] = new FieldDefinitionBlock( + $this->getSettings(), + $this->getLevel() + 1, + $this->getUsed(), + $field, + ); + } + } + + protected function getPrefix(): string { + return '{'; + } + + protected function getSuffix(): string { + return '}'; + } + + protected function isWrapped(): bool { + return true; + } + + protected function isNormalized(): bool { + return $this->getSettings()->isNormalizeFields(); + } + + protected function isAlwaysMultiline(): bool { + return true; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/ImplementsInterfacesList.php b/src/SchemaPrinter/Blocks/Types/ImplementsInterfacesList.php new file mode 100644 index 00000000..83ce6a4d --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/ImplementsInterfacesList.php @@ -0,0 +1,37 @@ + + */ +class ImplementsInterfacesList extends UsageList { + protected function block(mixed $item): Block { + return new TypeBlock( + $this->getSettings(), + $this->getLevel() + 1, + $this->getUsed(), + $item, + ); + } + + protected function separator(): string { + return '&'; + } + + protected function prefix(): string { + return 'implements'; + } + + protected function isNormalized(): bool { + return $this->getSettings()->isNormalizeInterfaces(); + } + + protected function isAlwaysMultiline(): bool { + return $this->getSettings()->isAlwaysMultilineInterfaces(); + } +} diff --git a/src/SchemaPrinter/Blocks/Types/InputFieldsDefinitionList.php b/src/SchemaPrinter/Blocks/Types/InputFieldsDefinitionList.php new file mode 100644 index 00000000..4e5a6d84 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/InputFieldsDefinitionList.php @@ -0,0 +1,55 @@ + + */ +class InputFieldsDefinitionList extends BlockList { + /** + * @param Traversable|array $fields + */ + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + Traversable|array $fields, + ) { + parent::__construct($settings, $level, $used); + + foreach ($fields as $field) { + $this[$field->name] = new InputValueDefinitionBlock( + $this->getSettings(), + $this->getLevel() + 1, + $this->getUsed(), + $field, + ); + } + } + + protected function getPrefix(): string { + return '{'; + } + + protected function getSuffix(): string { + return '}'; + } + + protected function isWrapped(): bool { + return true; + } + + protected function isNormalized(): bool { + return $this->getSettings()->isNormalizeFields(); + } + + protected function isAlwaysMultiline(): bool { + return true; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/InputObjectTypeDefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/InputObjectTypeDefinitionBlock.php new file mode 100644 index 00000000..dc33a211 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/InputObjectTypeDefinitionBlock.php @@ -0,0 +1,48 @@ + + */ +class InputObjectTypeDefinitionBlock extends DefinitionBlock { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + InputObjectType $definition, + ) { + parent::__construct($settings, $level, $used, $definition); + } + + protected function type(): string|null { + return 'input'; + } + + protected function body(int $used): Block|string|null { + return null; + } + + protected function fields(int $used): Block|string|null { + $definition = $this->getDefinition(); + $space = $this->space(); + $fields = $this->addUsed( + new InputFieldsDefinitionList( + $this->getSettings(), + $this->getLevel(), + $used + mb_strlen($space), + $definition->getFields(), + ), + ); + + return $fields; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/InputObjectTypeDefinitionBlockTest.php b/src/SchemaPrinter/Blocks/Types/InputObjectTypeDefinitionBlockTest.php new file mode 100644 index 00000000..f93306b1 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/InputObjectTypeDefinitionBlockTest.php @@ -0,0 +1,189 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + InputObjectType $definition, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new InputObjectTypeDefinitionBlock( + $settings, + $level, + $used, + $definition, + )); + + Parser::inputObjectTypeDefinition($actual); + + self::assertEquals($expected, $actual); + } + + /** + * @covers ::__toString + */ + public function testStatistics(): void { + $settings = new TestSettings(); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $definition = new InputObjectType([ + 'name' => 'A', + 'fields' => [ + 'b' => [ + 'name' => 'b', + 'type' => new InputObjectType([ + 'name' => 'B', + ]), + 'astNode' => Parser::fieldDefinition('b: B @a'), + ], + ], + 'astNode' => Parser::inputObjectTypeDefinition('input A @b'), + ]); + $block = new InputObjectTypeDefinitionBlock($settings, 0, 0, $definition); + + self::assertNotEmpty((string) $block); + self::assertEquals(['B' => 'B'], $block->getUsedTypes()); + self::assertEquals(['@a' => '@a', '@b' => '@b'], $block->getUsedDirectives()); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = (new TestSettings()) + ->setNormalizeFields(false); + + return [ + 'description + directives' => [ + <<<'STRING' + """ + Description + """ + input Test + @a + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + new InputObjectType([ + 'name' => 'Test', + 'astNode' => Parser::inputObjectTypeDefinition('input Test @a'), + 'description' => 'Description', + ]), + ], + 'description + directives + fields' => [ + <<<'STRING' + """ + Description + """ + input Test + @a + { + c: C + + """ + Description + """ + b: B + + a: A + } + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + new InputObjectType([ + 'name' => 'Test', + 'astNode' => Parser::inputObjectTypeDefinition('input Test @a'), + 'description' => 'Description', + 'fields' => [ + [ + 'name' => 'c', + 'type' => new InputObjectType([ + 'name' => 'C', + ]), + ], + [ + 'name' => 'b', + 'type' => new InputObjectType([ + 'name' => 'B', + ]), + 'description' => 'Description', + ], + [ + 'name' => 'a', + 'type' => new InputObjectType([ + 'name' => 'A', + ]), + ], + ], + ]), + ], + 'fields' => [ + <<<'STRING' + input Test { + a: String + } + STRING, + $settings, + 0, + 0, + new InputObjectType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + ]), + ], + 'indent' => [ + <<<'STRING' + input Test { + a: String + } + STRING, + $settings->setNormalizeInterfaces(true), + 1, + 120, + new InputObjectType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + ]), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/InputValueDefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/InputValueDefinitionBlock.php new file mode 100644 index 00000000..f6da2bae --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/InputValueDefinitionBlock.php @@ -0,0 +1,66 @@ + + */ +class InputValueDefinitionBlock extends DefinitionBlock { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + FieldArgument|InputObjectField $definition, + ) { + parent::__construct($settings, $level, $used, $definition); + } + + protected function type(): string|null { + return null; + } + + protected function body(int $used): Block|string|null { + $definition = $this->getDefinition(); + $space = $this->space(); + $type = $this->addUsed( + new TypeBlock( + $this->getSettings(), + $this->getLevel(), + $this->getUsed(), + $definition->getType(), + ), + ); + $body = ":{$space}{$type}"; + + if ($definition->defaultValueExists()) { + $prefix = "{$body}{$space}={$space}"; + $value = $this->addUsed( + new ValueNodeBlock( + $this->getSettings(), + $this->getLevel(), + $this->getUsed() + mb_strlen($prefix), + AST::astFromValue($definition->defaultValue, $definition->getType()) ?? new NullValueNode([]), + ), + ); + $body = "{$prefix}{$value}"; + } + + return $body; + } + + protected function fields(int $used): Block|string|null { + return null; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/InputValueDefinitionBlockTest.php b/src/SchemaPrinter/Blocks/Types/InputValueDefinitionBlockTest.php new file mode 100644 index 00000000..57a5a82e --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/InputValueDefinitionBlockTest.php @@ -0,0 +1,162 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + FieldArgument $definition, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new InputValueDefinitionBlock($settings, $level, $used, $definition)); + + Parser::inputValueDefinition($actual); + + self::assertEquals($expected, $actual); + } + + /** + * @covers ::__toString + */ + public function testStatistics(): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), new TestSettings()); + $definition = new FieldArgument([ + 'name' => 'a', + 'type' => new NonNull( + new ObjectType([ + 'name' => 'A', + ]), + ), + 'astNode' => Parser::inputValueDefinition('test: Test! @a'), + ]); + + $block = new InputValueDefinitionBlock($settings, 0, 0, $definition); + + self::assertNotEmpty((string) $block); + self::assertEquals(['A' => 'A'], $block->getUsedTypes()); + self::assertEquals(['@a' => '@a'], $block->getUsedDirectives()); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = new TestSettings(); + + return [ + 'without value' => [ + <<<'STRING' + """ + Description + """ + test: Test! + @a + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + new FieldArgument([ + 'name' => 'test', + 'type' => new NonNull( + new ObjectType([ + 'name' => 'Test', + ]), + ), + 'astNode' => Parser::inputValueDefinition('test: Test! @a'), + 'description' => 'Description', + ]), + ], + 'with value (short)' => [ + <<<'STRING' + """ + Description + """ + test: [String!] = ["aaaaaaaaaaaaaaaaaaaaaaaaaa"] + STRING, + $settings, + 0, + 0, + new FieldArgument([ + 'name' => 'test', + 'type' => new ListOfType(new NonNull(Type::string())), + 'defaultValue' => [ + 'aaaaaaaaaaaaaaaaaaaaaaaaaa', + ], + 'description' => 'Description', + ]), + ], + 'with value (long)' => [ + <<<'STRING' + """ + Description + """ + test: [String!] = [ + "aaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + STRING, + $settings, + 0, + 120, + new FieldArgument([ + 'name' => 'test', + 'type' => new ListOfType(new NonNull(Type::string())), + 'defaultValue' => [ + 'aaaaaaaaaaaaaaaaaaaaaaaaaa', + ], + 'description' => 'Description', + ]), + ], + 'indent' => [ + <<<'STRING' + """ + Description + """ + test: [String!] = [ + "aaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + STRING, + $settings, + 1, + 70, + new FieldArgument([ + 'name' => 'test', + 'type' => new ListOfType(new NonNull(Type::string())), + 'defaultValue' => [ + 'aaaaaaaaaaaaaaaaaaaaaaaaaa', + ], + 'description' => 'Description', + ]), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/InterfaceTypeDefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/InterfaceTypeDefinitionBlock.php new file mode 100644 index 00000000..36ed4429 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/InterfaceTypeDefinitionBlock.php @@ -0,0 +1,26 @@ + + */ +class InterfaceTypeDefinitionBlock extends TypeDefinitionBlock { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + InterfaceType $definition, + ) { + parent::__construct($settings, $level, $used, $definition); + } + + protected function type(): string|null { + return 'interface'; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/InterfaceTypeDefinitionBlockTest.php b/src/SchemaPrinter/Blocks/Types/InterfaceTypeDefinitionBlockTest.php new file mode 100644 index 00000000..5257e6ba --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/InterfaceTypeDefinitionBlockTest.php @@ -0,0 +1,376 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + InterfaceType $definition, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new InterfaceTypeDefinitionBlock($settings, $level, $used, $definition)); + + Parser::interfaceTypeDefinition($actual); + + self::assertEquals($expected, $actual); + } + + /** + * @covers ::__toString + */ + public function testStatistics(): void { + $settings = new TestSettings(); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $definition = new InterfaceType([ + 'name' => 'A', + 'fields' => [ + 'b' => [ + 'name' => 'b', + 'type' => new ObjectType([ + 'name' => 'B', + ]), + 'args' => [ + 'c' => [ + 'type' => new ObjectType([ + 'name' => 'C', + ]), + 'astNode' => Parser::inputValueDefinition('c: C @c'), + ], + ], + 'astNode' => Parser::fieldDefinition('b: B @b'), + ], + ], + 'interfaces' => [ + new InterfaceType([ + 'name' => 'D', + ]), + ], + 'astNode' => Parser::interfaceTypeDefinition('interface A @a'), + ]); + $block = new InterfaceTypeDefinitionBlock($settings, 0, 0, $definition); + + self::assertNotEmpty((string) $block); + self::assertEquals(['B' => 'B', 'C' => 'C', 'D' => 'D'], $block->getUsedTypes()); + self::assertEquals(['@a' => '@a', '@b' => '@b', '@c' => '@c'], $block->getUsedDirectives()); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = (new TestSettings()) + ->setNormalizeFields(false) + ->setNormalizeInterfaces(false) + ->setAlwaysMultilineInterfaces(false); + + return [ + 'description + directives' => [ + <<<'STRING' + """ + Description + """ + interface Test + @a + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + new InterfaceType([ + 'name' => 'Test', + 'astNode' => Parser::interfaceTypeDefinition('interface Test @a'), + 'description' => 'Description', + ]), + ], + 'description + directives + fields' => [ + <<<'STRING' + """ + Description + """ + interface Test + @a + { + c: C + + """ + Description + """ + b(b: Int): B + + a(a: Int): A + } + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + new InterfaceType([ + 'name' => 'Test', + 'astNode' => Parser::interfaceTypeDefinition('interface Test @a'), + 'description' => 'Description', + 'fields' => [ + [ + 'name' => 'c', + 'type' => new ObjectType([ + 'name' => 'C', + ]), + ], + [ + 'name' => 'b', + 'type' => new ObjectType([ + 'name' => 'B', + ]), + 'args' => [ + 'b' => [ + 'type' => Type::int(), + ], + ], + 'description' => 'Description', + ], + [ + 'name' => 'a', + 'type' => new ObjectType([ + 'name' => 'A', + ]), + 'args' => [ + 'a' => [ + 'type' => Type::int(), + ], + ], + ], + ], + ]), + ], + 'fields' => [ + <<<'STRING' + interface Test { + a: String + } + STRING, + $settings, + 0, + 0, + new InterfaceType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + ]), + ], + 'implements + directives + fields' => [ + <<<'STRING' + interface Test implements B & A + @a + { + a: String + } + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + new InterfaceType([ + 'name' => 'Test', + 'astNode' => Parser::interfaceTypeDefinition('interface Test @a'), + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new InterfaceType(['name' => 'B']), + new InterfaceType(['name' => 'A']), + ], + ]), + ], + 'implements(multiline) + directives + fields' => [ + <<<'STRING' + interface Test + implements + & B + & A + @a + { + a: String + } + STRING, + $settings->setPrintDirectives(true), + 0, + 120, + new InterfaceType([ + 'name' => 'Test', + 'astNode' => Parser::interfaceTypeDefinition('interface Test @a'), + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new InterfaceType(['name' => 'B']), + new InterfaceType(['name' => 'A']), + ], + ]), + ], + 'implements(multiline) + fields' => [ + <<<'STRING' + interface Test + implements + & B + & A + { + a: String + } + STRING, + $settings, + 0, + 120, + new InterfaceType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new InterfaceType(['name' => 'B']), + new InterfaceType(['name' => 'A']), + ], + ]), + ], + 'implements + fields' => [ + <<<'STRING' + interface Test implements B & A { + a: String + } + STRING, + $settings, + 0, + 0, + new InterfaceType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new InterfaceType(['name' => 'B']), + new InterfaceType(['name' => 'A']), + ], + ]), + ], + 'implements(normalized) + fields' => [ + <<<'STRING' + interface Test + implements + & A + & B + { + a: String + } + STRING, + $settings->setNormalizeInterfaces(true), + 0, + 120, + new InterfaceType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new InterfaceType(['name' => 'B']), + new InterfaceType(['name' => 'A']), + ], + ]), + ], + 'indent' => [ + <<<'STRING' + interface Test + implements + & A + & B + { + a: String + } + STRING, + $settings->setNormalizeInterfaces(true), + 1, + 120, + new InterfaceType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new InterfaceType(['name' => 'B']), + new InterfaceType(['name' => 'A']), + ], + ]), + ], + 'implements always multiline' => [ + <<<'STRING' + interface Test + implements + & A + { + a: String + } + STRING, + $settings + ->setAlwaysMultilineInterfaces(true), + 0, + 0, + new InterfaceType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new InterfaceType(['name' => 'A']), + ], + ]), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/ObjectTypeDefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/ObjectTypeDefinitionBlock.php new file mode 100644 index 00000000..ebe3419a --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/ObjectTypeDefinitionBlock.php @@ -0,0 +1,26 @@ + + */ +class ObjectTypeDefinitionBlock extends TypeDefinitionBlock { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + ObjectType $definition, + ) { + parent::__construct($settings, $level, $used, $definition); + } + + protected function type(): string|null { + return 'type'; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/ObjectTypeDefinitionBlockTest.php b/src/SchemaPrinter/Blocks/Types/ObjectTypeDefinitionBlockTest.php new file mode 100644 index 00000000..dd3315da --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/ObjectTypeDefinitionBlockTest.php @@ -0,0 +1,376 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + ObjectType $definition, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new ObjectTypeDefinitionBlock($settings, $level, $used, $definition)); + + Parser::objectTypeDefinition($actual); + + self::assertEquals($expected, $actual); + } + + /** + * @covers ::__toString + */ + public function testStatistics(): void { + $settings = new TestSettings(); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $definition = new ObjectType([ + 'name' => 'A', + 'fields' => [ + 'b' => [ + 'name' => 'b', + 'type' => new ObjectType([ + 'name' => 'B', + ]), + 'args' => [ + 'c' => [ + 'type' => new ObjectType([ + 'name' => 'C', + ]), + 'astNode' => Parser::inputValueDefinition('c: C @c'), + ], + ], + 'astNode' => Parser::fieldDefinition('b: B @b'), + ], + ], + 'interfaces' => [ + new InterfaceType([ + 'name' => 'D', + ]), + ], + 'astNode' => Parser::objectTypeDefinition('type A @a'), + ]); + $block = new ObjectTypeDefinitionBlock($settings, 0, 0, $definition); + + self::assertNotEmpty((string) $block); + self::assertEquals(['B' => 'B', 'C' => 'C', 'D' => 'D'], $block->getUsedTypes()); + self::assertEquals(['@a' => '@a', '@b' => '@b', '@c' => '@c'], $block->getUsedDirectives()); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = (new TestSettings()) + ->setNormalizeFields(false) + ->setNormalizeInterfaces(false) + ->setAlwaysMultilineInterfaces(false); + + return [ + 'description + directives' => [ + <<<'STRING' + """ + Description + """ + type Test + @a + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + new ObjectType([ + 'name' => 'Test', + 'astNode' => Parser::objectTypeDefinition('type Test @a'), + 'description' => 'Description', + ]), + ], + 'description + directives + fields' => [ + <<<'STRING' + """ + Description + """ + type Test + @a + { + c: C + + """ + Description + """ + b(b: Int): B + + a(a: Int): A + } + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + new ObjectType([ + 'name' => 'Test', + 'astNode' => Parser::objectTypeDefinition('type Test @a'), + 'description' => 'Description', + 'fields' => [ + [ + 'name' => 'c', + 'type' => new ObjectType([ + 'name' => 'C', + ]), + ], + [ + 'name' => 'b', + 'type' => new ObjectType([ + 'name' => 'B', + ]), + 'args' => [ + 'b' => [ + 'type' => Type::int(), + ], + ], + 'description' => 'Description', + ], + [ + 'name' => 'a', + 'type' => new ObjectType([ + 'name' => 'A', + ]), + 'args' => [ + 'a' => [ + 'type' => Type::int(), + ], + ], + ], + ], + ]), + ], + 'fields' => [ + <<<'STRING' + type Test { + a: String + } + STRING, + $settings, + 0, + 0, + new ObjectType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + ]), + ], + 'implements + directives + fields' => [ + <<<'STRING' + type Test implements B & A + @a + { + a: String + } + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + new ObjectType([ + 'name' => 'Test', + 'astNode' => Parser::objectTypeDefinition('type Test @a'), + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new ObjectType(['name' => 'B']), + new ObjectType(['name' => 'A']), + ], + ]), + ], + 'implements(multiline) + directives + fields' => [ + <<<'STRING' + type Test + implements + & B + & A + @a + { + a: String + } + STRING, + $settings->setPrintDirectives(true), + 0, + 120, + new ObjectType([ + 'name' => 'Test', + 'astNode' => Parser::objectTypeDefinition('type Test @a'), + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new ObjectType(['name' => 'B']), + new ObjectType(['name' => 'A']), + ], + ]), + ], + 'implements(multiline) + fields' => [ + <<<'STRING' + type Test + implements + & B + & A + { + a: String + } + STRING, + $settings, + 0, + 120, + new ObjectType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new ObjectType(['name' => 'B']), + new ObjectType(['name' => 'A']), + ], + ]), + ], + 'implements + fields' => [ + <<<'STRING' + type Test implements B & A { + a: String + } + STRING, + $settings, + 0, + 0, + new ObjectType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new ObjectType(['name' => 'B']), + new ObjectType(['name' => 'A']), + ], + ]), + ], + 'implements(normalized) + fields' => [ + <<<'STRING' + type Test + implements + & A + & B + { + a: String + } + STRING, + $settings->setNormalizeInterfaces(true), + 0, + 120, + new ObjectType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new ObjectType(['name' => 'B']), + new ObjectType(['name' => 'A']), + ], + ]), + ], + 'indent' => [ + <<<'STRING' + type Test + implements + & A + & B + { + a: String + } + STRING, + $settings->setNormalizeInterfaces(true), + 1, + 120, + new ObjectType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new ObjectType(['name' => 'B']), + new ObjectType(['name' => 'A']), + ], + ]), + ], + 'implements always multiline' => [ + <<<'STRING' + type Test + implements + & B + { + a: String + } + STRING, + $settings + ->setAlwaysMultilineInterfaces(true), + 0, + 0, + new ObjectType([ + 'name' => 'Test', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::string(), + ], + ], + 'interfaces' => [ + new ObjectType(['name' => 'B']), + ], + ]), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/OperationType.php b/src/SchemaPrinter/Blocks/Types/OperationType.php new file mode 100644 index 00000000..5e9f64c8 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/OperationType.php @@ -0,0 +1,23 @@ +operation; + } + + protected function content(): string { + $content = parent::content(); + $content = "{$this->getOperation()}:{$this->space()}{$content}"; + + return $content; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/RootOperationTypesDefinitionList.php b/src/SchemaPrinter/Blocks/Types/RootOperationTypesDefinitionList.php new file mode 100644 index 00000000..30b29377 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/RootOperationTypesDefinitionList.php @@ -0,0 +1,22 @@ +getSettings()->isNormalizeFields(); + } + + protected function isAlwaysMultiline(): bool { + return true; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/ScalarTypeDefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/ScalarTypeDefinitionBlock.php new file mode 100644 index 00000000..545bddd5 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/ScalarTypeDefinitionBlock.php @@ -0,0 +1,35 @@ + + */ +class ScalarTypeDefinitionBlock extends DefinitionBlock { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + ScalarType $definition, + ) { + parent::__construct($settings, $level, $used, $definition); + } + + protected function type(): string|null { + return 'scalar'; + } + + protected function body(int $used): Block|string|null { + return null; + } + + protected function fields(int $used): Block|string|null { + return null; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/ScalarTypeDefinitionBlockTest.php b/src/SchemaPrinter/Blocks/Types/ScalarTypeDefinitionBlockTest.php new file mode 100644 index 00000000..fdc351de --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/ScalarTypeDefinitionBlockTest.php @@ -0,0 +1,153 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + ScalarType $type, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new ScalarTypeDefinitionBlock($settings, $level, $used, $type)); + + Parser::scalarTypeDefinition($actual); + + self::assertEquals($expected, $actual); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = (new TestSettings()) + ->setPrintDirectives(false); + + return [ + 'scalar' => [ + <<<'STRING' + scalar Test + STRING, + $settings, + 0, + 0, + new CustomScalarType([ + 'name' => 'Test', + ]), + ], + 'with description and directives' => [ + <<<'STRING' + """ + Description + """ + scalar Test + @a + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + new CustomScalarType([ + 'name' => 'Test', + 'description' => 'Description', + 'astNode' => Parser::scalarTypeDefinition( + <<<'STRING' + scalar Test @a + STRING, + ), + ]), + ], + 'with directives in description' => [ + <<<'STRING' + """ + Description + + @a + """ + scalar Test + STRING, + $settings->setPrintDirectivesInDescription(true), + 0, + 0, + new CustomScalarType([ + 'name' => 'Test', + 'description' => 'Description', + 'astNode' => Parser::scalarTypeDefinition( + <<<'STRING' + scalar Test @a + STRING, + ), + ]), + ], + 'indent' => [ + <<<'STRING' + """ + Description + """ + scalar Test + @a( + value: "very very long value" + ) + @b(value: "b") + STRING, + $settings->setPrintDirectives(true), + 1, + 60, + new CustomScalarType([ + 'name' => 'Test', + 'description' => 'Description', + 'astNode' => Parser::scalarTypeDefinition( + <<<'STRING' + scalar Test @a(value: "very very long value") @b(value: "b") + STRING, + ), + ]), + ], + 'indent + no description' => [ + <<<'STRING' + scalar Test + @a( + value: "very very long value" + ) + @b(value: "b") + STRING, + $settings->setPrintDirectives(true), + 1, + 60, + new CustomScalarType([ + 'name' => 'Test', + 'astNode' => Parser::scalarTypeDefinition( + <<<'STRING' + scalar Test @a(value: "very very long value") @b(value: "b") + STRING, + ), + ]), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/SchemaDefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/SchemaDefinitionBlock.php new file mode 100644 index 00000000..7c7e3fc9 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/SchemaDefinitionBlock.php @@ -0,0 +1,103 @@ + + */ +class SchemaDefinitionBlock extends DefinitionBlock { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + Schema $definition, + ) { + parent::__construct($settings, $level, $used, $definition); + } + + protected function type(): string|null { + return 'schema'; + } + + protected function content(): string { + $content = parent::content(); + + if ($this->isUseDefaultRootOperationTypeNames()) { + $content = ''; + } + + return $content; + } + + protected function body(int $used): Block|string|null { + return null; + } + + protected function fields(int $used): Block|string|null { + $definition = $this->getDefinition(); + $space = $this->space(); + $fields = new RootOperationTypesDefinitionList( + $this->getSettings(), + $this->getLevel(), + $used + mb_strlen($space), + ); + $types = [ + [OperationType::query(), $definition->getQueryType()], + [OperationType::mutation(), $definition->getMutationType()], + [OperationType::subscription(), $definition->getSubscriptionType()], + ]; + + foreach ($types as $config) { + [$operation, $type] = $config; + + if ($type) { + $fields[] = new RootOperationTypeDefinitionBlock( + $this->getSettings(), + $this->getLevel() + 1, + $this->getUsed(), + $operation, + $type, + ); + } + } + + return $this->addUsed($fields); + } + + public function isUseDefaultRootOperationTypeNames(): bool { + // Directives? + if (count($this->getDefinitionDirectives()) > 0) { + return false; + } + + // Names? + $definition = $this->getDefinition(); + $rootTypes = [ + 'Query' => $definition->getQueryType(), + 'Mutation' => $definition->getMutationType(), + 'Subscription' => $definition->getSubscriptionType(), + ]; + $nonStandard = array_filter( + $rootTypes, + static function (?ObjectType $type, string $name): bool { + return $type !== null && $type->name !== $name; + }, + ARRAY_FILTER_USE_BOTH, + ); + + return !$nonStandard; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/SchemaDefinitionBlockTest.php b/src/SchemaPrinter/Blocks/Types/SchemaDefinitionBlockTest.php new file mode 100644 index 00000000..27d3d409 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/SchemaDefinitionBlockTest.php @@ -0,0 +1,155 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + Schema $schema, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new SchemaDefinitionBlock($settings, $level, $used, $schema)); + + if ($expected && !str_starts_with($actual, '"""')) { + // https://github.com/webonyx/graphql-php/issues/1027 + Parser::schemaDefinition($actual); + } + + self::assertEquals($expected, $actual); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = (new TestSettings()) + ->setPrintDirectives(false) + ->setNormalizeFields(false); + + return [ + 'standard names' => [ + '', + $settings, + 0, + 0, + new Schema([ + 'query' => new ObjectType(['name' => 'Query']), + 'mutation' => new ObjectType(['name' => 'Mutation']), + 'subscription' => new ObjectType(['name' => 'Subscription']), + ]), + ], + 'standard names with directives' => [ + <<<'STRING' + schema + @a + { + query: Query + mutation: Mutation + subscription: Subscription + } + STRING, + $settings->setPrintDirectives(true), + 0, + 0, + new Schema([ + 'query' => new ObjectType(['name' => 'Query']), + 'mutation' => new ObjectType(['name' => 'Mutation']), + 'subscription' => new ObjectType(['name' => 'Subscription']), + 'astNode' => Parser::schemaDefinition( + <<<'STRING' + schema @a { query: Query } + STRING, + ), + ]), + ], + 'standard names with directives in description' => [ + <<<'STRING' + """ + @a + """ + schema { + query: Query + mutation: Mutation + subscription: Subscription + } + STRING, + $settings->setPrintDirectivesInDescription(true), + 0, + 0, + new Schema([ + 'query' => new ObjectType(['name' => 'Query']), + 'mutation' => new ObjectType(['name' => 'Mutation']), + 'subscription' => new ObjectType(['name' => 'Subscription']), + 'astNode' => Parser::schemaDefinition( + <<<'STRING' + schema @a { query: Query } + STRING, + ), + ]), + ], + 'non standard names' => [ + <<<'STRING' + schema { + query: MyQuery + mutation: Mutation + subscription: Subscription + } + STRING, + $settings, + 0, + 0, + new Schema([ + 'query' => new ObjectType(['name' => 'MyQuery']), + 'mutation' => new ObjectType(['name' => 'Mutation']), + 'subscription' => new ObjectType(['name' => 'Subscription']), + ]), + ], + 'indent' => [ + <<<'STRING' + schema { + query: MyQuery + mutation: Mutation + subscription: Subscription + } + STRING, + $settings, + 1, + 0, + new Schema([ + 'query' => new ObjectType(['name' => 'MyQuery']), + 'mutation' => new ObjectType(['name' => 'Mutation']), + 'subscription' => new ObjectType(['name' => 'Subscription']), + ]), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/StringBlock.php b/src/SchemaPrinter/Blocks/Types/StringBlock.php new file mode 100644 index 00000000..08a98de6 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/StringBlock.php @@ -0,0 +1,78 @@ +string; + } + + protected function isBlock(): bool { + return false; + } + + protected function content(): string { + // Begin + $eol = $this->eol(); + $indent = $this->indent(); + $wrapper = '"""'; + $content = $this->getString(); + + // Whitespace only? (it cannot be rendered as BlockString) + if (preg_match('/^\h+$/u', $content)) { + return Printer::doPrint( + new StringValueNode([ + 'value' => $content, + 'block' => false, + ]), + ); + } + + // Multiline? + $length = $this->getUsed() + mb_strlen($indent) + 2 * mb_strlen($wrapper) + mb_strlen($content); + $isOneliner = !$this->isStringMultiline($content); + $isMultiline = $this->isBlock() + || !$isOneliner + || $this->isLineTooLong($length) + || str_ends_with($content, '"') + || str_ends_with($content, '\\\\'); + + if ($isOneliner && (bool) preg_match('/^\h+/u', $content)) { + $isMultiline = false; + } + + if ($isMultiline && $content !== '') { + $content = $eol.preg_replace('/(.+)/mu', "{$indent}\$1", $content).$eol.$indent; + } + + // Wrap && Escape + $content = str_replace($wrapper, "\\{$wrapper}", $content); + $content = "{$wrapper}{$content}{$wrapper}"; + + // Return + return $content; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/StringBlockTest.php b/src/SchemaPrinter/Blocks/Types/StringBlockTest.php new file mode 100644 index 00000000..e1f87613 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/StringBlockTest.php @@ -0,0 +1,227 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + string $string, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) new StringBlock($settings, $level, $used, $string); + $parsed = Parser::valueLiteral($actual); + + self::assertInstanceOf(StringValueNode::class, $parsed); + self::assertEquals($expected, $actual); + self::assertEquals($string, $parsed->value); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = new TestSettings(); + + return [ + 'Prints an empty string' => [ + '""""""', + $settings, + 0, + 0, + '', + ], + 'Prints an string with only whitespace' => [ + '" "', + $settings, + 0, + 0, + ' ', + ], + 'One-line prints a short string' => [ + '"""Short string"""', + $settings, + 0, + 0, + 'Short string', + ], + 'One-line prints a long string' => [ + <<<'STRING' + """ + Long string + """ + STRING, + $settings->setLineLength(4), + 0, + 0, + 'Long string', + ], + 'String is short (indent)' => [ + <<<'STRING' + """string""" + STRING, + $settings->setLineLength(21), + 2, + 0, + 'string', + ], + 'String is long (indent)' => [ + <<<'STRING' + """ + string + """ + STRING, + $settings + ->setIndent(' ') + ->setLineLength(22), + 2, + 20, + 'string', + ], + 'Multi-line string' => [ + <<<'STRING' + """ + aaa + bbb + + ccc + """ + STRING, + $settings, + 0, + 0, + <<<'STRING' + aaa + bbb + + ccc + STRING, + ], + 'Leading space' => [ + <<<'STRING' + """ Leading space""" + STRING, + $settings, + 0, + 0, + ' Leading space', + ], + 'Leading tab' => [ + "\"\"\"\tLeading tab\"\"\"", + $settings, + 0, + 0, + "\tLeading tab", + ], + 'Leading whitespace (single line)' => [ + "\"\"\"\tLeading tab\"\"\"", + $settings->setLineLength(1), + 0, + 0, + "\tLeading tab", + ], + 'Trailing "' => [ + <<<'STRING' + """ + Trailing " + """ + STRING, + $settings, + 0, + 0, + 'Trailing "', + ], + 'Leading whitespace and trailing "' => [ + <<<'STRING' + """ + Leading whitespace and trailing " + abc + """ + STRING, + $settings, + 0, + 0, + <<<'STRING' + Leading whitespace and trailing " + abc + STRING, + ], + 'Trailing backslash' => [ + <<<'STRING' + """ + Trailing \\ + """ + STRING, + $settings, + 0, + 0, + 'Trailing \\\\', + ], + 'Escape wrapper' => [ + <<<'STRING' + """String with \""" wrapper""" + STRING, + $settings, + 0, + 0, + 'String with """ wrapper', + ], + 'Indent' => [ + implode( + "\n", + [ + '"""', + ' aaa', + '', + ' bbb ', + ' ccc ', + ' ', + ' ddd ', + ' """', + ], + ), + $settings->setIndent(' '), + 2, + 0, + implode( + "\n", + [ + ' aaa', + '', + ' bbb ', + 'ccc ', + ' ', + ' ddd ', + ], + ), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/TypeBlock.php b/src/SchemaPrinter/Blocks/Types/TypeBlock.php new file mode 100644 index 00000000..5ecf59c6 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/TypeBlock.php @@ -0,0 +1,43 @@ +getType(); + + if ($type instanceof WrappingType) { + $type = $type->getWrappedType(true); + } + + return $type->name; + } + + protected function getType(): Type { + return $this->definition; + } + + protected function content(): string { + $this->addUsedType($this->getName()); + + return (string) $this->getType(); + } +} diff --git a/src/SchemaPrinter/Blocks/Types/TypeBlockTest.php b/src/SchemaPrinter/Blocks/Types/TypeBlockTest.php new file mode 100644 index 00000000..ea1be3f3 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/TypeBlockTest.php @@ -0,0 +1,105 @@ + + // ========================================================================= + /** + * @covers ::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + Type $type, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new TypeBlock($settings, $level, $used, $type)); + + self::assertEquals($expected, $actual); + } + + /** + * @covers ::__toString + */ + public function testStatistics(): void { + $node = new NonNull( + new ObjectType([ + 'name' => 'Test', + ]), + ); + $settings = new TestSettings(); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $block = new TypeBlock($settings, 0, 0, $node); + $type = $node->getWrappedType(true)->name; + + self::assertNotEmpty((string) $block); + self::assertEquals([$type => $type], $block->getUsedTypes()); + self::assertEquals([], $block->getUsedDirectives()); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = new TestSettings(); + + return [ + 'object' => [ + 'Test', + $settings, + 0, + 0, + new ObjectType([ + 'name' => 'Test', + ]), + ], + 'non null' => [ + 'Test!', + $settings, + 0, + 0, + new NonNull( + new ObjectType([ + 'name' => 'Test', + ]), + ), + ], + 'non null list' => [ + '[Test]!', + $settings, + 0, + 0, + new NonNull( + new ListOfType( + new ObjectType([ + 'name' => 'Test', + ]), + ), + ), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/TypeDefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/TypeDefinitionBlock.php new file mode 100644 index 00000000..aa6b0a48 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/TypeDefinitionBlock.php @@ -0,0 +1,69 @@ + + */ +abstract class TypeDefinitionBlock extends DefinitionBlock { + /** + * @param TType $definition + */ + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + InterfaceType|ObjectType $definition, + ) { + parent::__construct($settings, $level, $used, $definition); + } + + protected function body(int $used): Block|string|null { + $definition = $this->getDefinition(); + $space = $this->space(); + $interfaces = $this->addUsed( + new ImplementsInterfacesList( + $this->getSettings(), + $this->getLevel() + 1, + $used + mb_strlen($space), + $definition->getInterfaces(), + ), + ); + + if (!$interfaces->isEmpty()) { + if ($interfaces->isMultiline()) { + $eol = $this->eol(); + $indent = $this->indent($this->getLevel()); + $interfaces = "{$eol}{$indent}{$interfaces}"; + } else { + $interfaces = "{$space}{$interfaces}"; + } + } + + return $interfaces; + } + + protected function fields(int $used): Block|string|null { + $definition = $this->getDefinition(); + $space = $this->space(); + $fields = new FieldsDefinitionList( + $this->getSettings(), + $this->getLevel(), + $used + mb_strlen($space), + $definition->getFields(), + ); + + return $this->addUsed($fields); + } +} diff --git a/src/SchemaPrinter/Blocks/Types/UnionMemberTypesList.php b/src/SchemaPrinter/Blocks/Types/UnionMemberTypesList.php new file mode 100644 index 00000000..9cf0f673 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/UnionMemberTypesList.php @@ -0,0 +1,37 @@ + + */ +class UnionMemberTypesList extends UsageList { + protected function block(mixed $item): Block { + return new TypeBlock( + $this->getSettings(), + $this->getLevel() + 1, + $this->getUsed(), + $item, + ); + } + + protected function separator(): string { + return '|'; + } + + protected function prefix(): string { + return ''; + } + + protected function isNormalized(): bool { + return $this->getSettings()->isNormalizeUnions(); + } + + protected function isAlwaysMultiline(): bool { + return $this->getSettings()->isAlwaysMultilineUnions(); + } +} diff --git a/src/SchemaPrinter/Blocks/Types/UnionTypeDefinitionBlock.php b/src/SchemaPrinter/Blocks/Types/UnionTypeDefinitionBlock.php new file mode 100644 index 00000000..9b0fbdb7 --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/UnionTypeDefinitionBlock.php @@ -0,0 +1,56 @@ + + */ +class UnionTypeDefinitionBlock extends DefinitionBlock { + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + UnionType $definition, + ) { + parent::__construct($settings, $level, $used, $definition); + } + + protected function type(): string|null { + return 'union'; + } + + protected function body(int $used): Block|string|null { + return null; + } + + protected function fields(int $used): Block|string|null { + $space = $this->space(); + $equal = "={$space}"; + $types = $this->addUsed( + new UnionMemberTypesList( + $this->getSettings(), + $this->getLevel() + 1, + $used + mb_strlen($equal), + $this->getDefinition()->getTypes(), + ), + ); + + if ($types->isMultiline()) { + $eol = $this->eol(); + $indent = $this->indent($this->getLevel() + 1); + $types = "={$eol}{$indent}{$types}"; + } else { + $types = "{$equal}{$types}"; + } + + return $types; + } +} diff --git a/src/SchemaPrinter/Blocks/Types/UnionTypeDefinitionBlockTest.php b/src/SchemaPrinter/Blocks/Types/UnionTypeDefinitionBlockTest.php new file mode 100644 index 00000000..612accba --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/UnionTypeDefinitionBlockTest.php @@ -0,0 +1,301 @@ + + // ========================================================================= + /** + * @covers ::__toString + * @covers \LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Types\UnionMemberTypesList::__toString + * + * @dataProvider dataProviderToString + */ + public function testToString( + string $expected, + Settings $settings, + int $level, + int $used, + UnionType $type, + ): void { + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $actual = (string) (new UnionTypeDefinitionBlock($settings, $level, $used, $type)); + + Parser::unionTypeDefinition($actual); + + self::assertEquals($expected, $actual); + } + + /** + * @covers ::__toString + */ + public function testStatistics(): void { + $union = new UnionType([ + 'name' => 'Test', + 'types' => [ + new ObjectType([ + 'name' => 'A', + ]), + new ObjectType([ + 'name' => 'B', + ]), + ], + 'astNode' => Parser::unionTypeDefinition('union Test @a = A | B'), + ]); + $settings = new TestSettings(); + $settings = new PrinterSettings($this->app->make(DirectiveResolver::class), $settings); + $block = new UnionTypeDefinitionBlock($settings, 0, 0, $union); + + self::assertNotEmpty((string) $block); + self::assertEquals(['A' => 'A', 'B' => 'B'], $block->getUsedTypes()); + self::assertEquals(['@a' => '@a'], $block->getUsedDirectives()); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderToString(): array { + $settings = (new TestSettings()) + ->setNormalizeUnions(false) + ->setAlwaysMultilineUnions(false); + + return [ + 'single-line' => [ + <<<'STRING' + union Test = C | B | A + STRING, + $settings, + 0, + 0, + new UnionType([ + 'name' => 'Test', + 'types' => [ + new ObjectType([ + 'name' => 'C', + ]), + new ObjectType([ + 'name' => 'B', + ]), + new ObjectType([ + 'name' => 'A', + ]), + ], + ]), + ], + 'multiline' => [ + <<<'STRING' + union Test = + | C + | B + | A + STRING, + $settings, + 0, + 120, + new UnionType([ + 'name' => 'Test', + 'types' => [ + new ObjectType([ + 'name' => 'C', + ]), + new ObjectType([ + 'name' => 'B', + ]), + new ObjectType([ + 'name' => 'A', + ]), + ], + ]), + ], + 'indent single-line' => [ + <<<'STRING' + union Test = C | B | A + STRING, + $settings, + 1, + 0, + new UnionType([ + 'name' => 'Test', + 'types' => [ + new ObjectType([ + 'name' => 'C', + ]), + new ObjectType([ + 'name' => 'B', + ]), + new ObjectType([ + 'name' => 'A', + ]), + ], + ]), + ], + 'indent multiline' => [ + <<<'STRING' + union Test = + | C + | B + | A + STRING, + $settings, + 1, + 120, + new UnionType([ + 'name' => 'Test', + 'types' => [ + new ObjectType([ + 'name' => 'C', + ]), + new ObjectType([ + 'name' => 'B', + ]), + new ObjectType([ + 'name' => 'A', + ]), + ], + ]), + ], + 'multiline normalized' => [ + <<<'STRING' + union Test = A | B | C + STRING, + $settings->setNormalizeUnions(true), + 0, + 0, + new UnionType([ + 'name' => 'Test', + 'types' => [ + new ObjectType([ + 'name' => 'C', + ]), + new ObjectType([ + 'name' => 'B', + ]), + new ObjectType([ + 'name' => 'A', + ]), + ], + ]), + ], + 'multiline always' => [ + <<<'STRING' + union Test = + | C + | B + | A + STRING, + $settings->setAlwaysMultilineUnions(true), + 0, + 0, + new UnionType([ + 'name' => 'Test', + 'types' => [ + new ObjectType([ + 'name' => 'C', + ]), + new ObjectType([ + 'name' => 'B', + ]), + new ObjectType([ + 'name' => 'A', + ]), + ], + ]), + ], + 'directives' => [ + <<<'STRING' + union Test + @a + = C | B | A + STRING, + $settings, + 0, + 0, + new UnionType([ + 'name' => 'Test', + 'types' => [ + new ObjectType([ + 'name' => 'C', + ]), + new ObjectType([ + 'name' => 'B', + ]), + new ObjectType([ + 'name' => 'A', + ]), + ], + 'astNode' => Parser::unionTypeDefinition( + <<<'STRING' + union Test @a = A | B | C + STRING, + ), + ]), + ], + 'directives + multiline' => [ + <<<'STRING' + union Test + @a + = + | C + | B + | A + STRING, + $settings, + 0, + 120, + new UnionType([ + 'name' => 'Test', + 'types' => [ + new ObjectType([ + 'name' => 'C', + ]), + new ObjectType([ + 'name' => 'B', + ]), + new ObjectType([ + 'name' => 'A', + ]), + ], + 'astNode' => Parser::unionTypeDefinition( + <<<'STRING' + union Test @a = A | B | C + STRING, + ), + ]), + ], + 'one member + always multiline' => [ + <<<'STRING' + union Test = + | A + STRING, + $settings->setAlwaysMultilineUnions(true), + 0, + 0, + new UnionType([ + 'name' => 'Test', + 'types' => [ + new ObjectType([ + 'name' => 'A', + ]), + ], + ]), + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/Blocks/Types/UsageList.php b/src/SchemaPrinter/Blocks/Types/UsageList.php new file mode 100644 index 00000000..975a53de --- /dev/null +++ b/src/SchemaPrinter/Blocks/Types/UsageList.php @@ -0,0 +1,85 @@ + + */ +abstract class UsageList extends BlockList { + /** + * @param Traversable|array $items + */ + public function __construct( + PrinterSettings $settings, + int $level, + int $used, + Traversable|array $items, + ) { + parent::__construct($settings, $level, $used); + + foreach ($items as $item) { + $this[] = $this->block($item); + } + } + + public function isMultiline(): bool { + return parent::isMultiline() || $this->isAlwaysMultiline(); + } + + /** + * @param TType $item + * + * @return TBlock + */ + abstract protected function block(mixed $item): Block; + + abstract protected function separator(): string; + + abstract protected function prefix(): string; + + protected function getSeparator(): string { + return "{$this->space()}{$this->separator()}{$this->space()}"; + } + + protected function getMultilineItemPrefix(): string { + return "{$this->separator()}{$this->space()}"; + } + + protected function content(): string { + $prefix = $this->prefix(); + $content = parent::content(); + + if ($content) { + if ($this->isAlwaysMultiline() || $this->isStringMultiline($content)) { + $eol = $this->eol(); + $indent = $this->indent(); + + if ($prefix) { + $content = "{$prefix}{$eol}{$indent}{$content}"; + } + } else { + $space = $this->space(); + + if ($prefix) { + $content = "{$prefix}{$space}{$content}"; + } + } + } + + return $content; + } + + protected function getUsed(): int { + return parent::getUsed() + mb_strlen("{$this->prefix()}{$this->space()}"); + } +} diff --git a/src/SchemaPrinter/Contracts/DirectiveFilter.php b/src/SchemaPrinter/Contracts/DirectiveFilter.php new file mode 100644 index 00000000..24116c3e --- /dev/null +++ b/src/SchemaPrinter/Contracts/DirectiveFilter.php @@ -0,0 +1,10 @@ + + */ + public function getUsedTypes(): array; + + /** + * @return array + */ + public function getUsedDirectives(): array; +} diff --git a/src/SchemaPrinter/Exceptions/Exception.php b/src/SchemaPrinter/Exceptions/Exception.php new file mode 100644 index 00000000..d548e67a --- /dev/null +++ b/src/SchemaPrinter/Exceptions/Exception.php @@ -0,0 +1,9 @@ +type, + ), + $previous, + ); + } + + public function getType(): Type { + return $this->type; + } +} diff --git a/src/SchemaPrinter/IntrospectionPrinter.php b/src/SchemaPrinter/IntrospectionPrinter.php new file mode 100644 index 00000000..4be0ec29 --- /dev/null +++ b/src/SchemaPrinter/IntrospectionPrinter.php @@ -0,0 +1,52 @@ +setPrintUnusedDefinitions(true) + ->setPrintDirectiveDefinitions(true), + ); + } + + protected function getTypeDefinitions(PrinterSettings $settings, Schema $schema): BlockList { + $blocks = $this->getDefinitionList($settings); + + foreach (Introspection::getTypes() as $type) { + $blocks[] = $this->getDefinitionBlock($settings, $type); + } + + return $blocks; + } + + protected function getDirectiveDefinitions(PrinterSettings $settings, Schema $schema): BlockList { + $blocks = $this->getDefinitionList($settings); + $directives = $schema->getDirectives(); + + foreach ($directives as $directive) { + if (Directive::isSpecifiedDirective($directive)) { + $blocks[] = $this->getDefinitionBlock($settings, $directive); + } + } + + return $blocks; + } +} diff --git a/src/SchemaPrinter/IntrospectionPrinterTest.php b/src/SchemaPrinter/IntrospectionPrinterTest.php new file mode 100644 index 00000000..7fd49fb9 --- /dev/null +++ b/src/SchemaPrinter/IntrospectionPrinterTest.php @@ -0,0 +1,58 @@ + + // ========================================================================= + /** + * @covers ::print + * + * @dataProvider dataProviderPrint + */ + public function testPrint(string $expected, Settings $settings, int $level): void { + $expected = $this->getTestData()->content($expected); + $printer = $this->app->make(IntrospectionPrinter::class)->setSettings($settings)->setLevel($level); + $schema = new Schema([]); + $actual = $printer->print($schema); + + self::assertEquals($expected, (string) $actual); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderPrint(): array { + return [ + GraphQLSettings::class => [ + '~graphql-settings.graphql', + new GraphQLSettings(), + 0, + ], + TestSettings::class => [ + '~test-settings.graphql', + new TestSettings(), + 0, + ], + TestSettings::class.' (level)' => [ + '~test-settings-level.graphql', + new TestSettings(), + 1, + ], + ]; + } + // +} diff --git a/src/SchemaPrinter/IntrospectionPrinterTest~graphql-settings.graphql b/src/SchemaPrinter/IntrospectionPrinterTest~graphql-settings.graphql new file mode 100644 index 00000000..9885cf4e --- /dev/null +++ b/src/SchemaPrinter/IntrospectionPrinterTest~graphql-settings.graphql @@ -0,0 +1,281 @@ +""" +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 variable definition. + """ + VARIABLE_DEFINITION + + """ + 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 +} + +""" +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`, `interfaces`, 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 +} + +""" +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 + args: [__InputValue!]! + isRepeatable: Boolean! + locations: [__DirectiveLocation!]! +} + +""" +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 +} + +""" +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 using the Markdown syntax (as specified by [CommonMark](https://commonmark.org/). + """ + reason: String = "No longer supported" +) +on + | FIELD_DEFINITION + | ENUM_VALUE + +""" +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 diff --git a/src/SchemaPrinter/IntrospectionPrinterTest~test-settings-level.graphql b/src/SchemaPrinter/IntrospectionPrinterTest~test-settings-level.graphql new file mode 100644 index 00000000..552f4d93 --- /dev/null +++ b/src/SchemaPrinter/IntrospectionPrinterTest~test-settings-level.graphql @@ -0,0 +1,281 @@ + """ + A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies. + """ + enum __DirectiveLocation { + """ + Location adjacent to an argument definition. + """ + ARGUMENT_DEFINITION + + """ + Location adjacent to an enum definition. + """ + ENUM + + """ + Location adjacent to an enum value definition. + """ + ENUM_VALUE + + """ + Location adjacent to a field. + """ + FIELD + + """ + Location adjacent to a field definition. + """ + FIELD_DEFINITION + + """ + 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 an input object field definition. + """ + INPUT_FIELD_DEFINITION + + """ + Location adjacent to an input object type definition. + """ + INPUT_OBJECT + + """ + Location adjacent to an interface definition. + """ + INTERFACE + + """ + Location adjacent to a mutation operation. + """ + MUTATION + + """ + Location adjacent to an object type definition. + """ + OBJECT + + """ + Location adjacent to a query operation. + """ + QUERY + + """ + Location adjacent to a scalar definition. + """ + SCALAR + + """ + Location adjacent to a schema definition. + """ + SCHEMA + + """ + Location adjacent to a subscription operation. + """ + SUBSCRIPTION + + """ + Location adjacent to a union definition. + """ + UNION + + """ + Location adjacent to a variable definition. + """ + VARIABLE_DEFINITION + } + + """ + An enum describing what kind of type a given `__Type` is. + """ + enum __TypeKind { + """ + 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 an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields. + """ + INTERFACE + + """ + 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 + + """ + Indicates this type is an object. `fields` and `interfaces` are valid fields. + """ + OBJECT + + """ + Indicates this type is a scalar. + """ + SCALAR + + """ + Indicates this type is a union. `possibleTypes` is a valid field. + """ + UNION + } + + """ + 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 { + args: [__InputValue!]! + description: String + isRepeatable: Boolean! + locations: [__DirectiveLocation!]! + name: String! + } + + """ + 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 { + deprecationReason: String + description: String + isDeprecated: Boolean! + name: 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 { + args: [__InputValue!]! + deprecationReason: String + description: String + isDeprecated: Boolean! + name: String! + type: __Type! + } + + """ + 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 { + """ + A GraphQL-formatted string representing the default value for this input value. + """ + defaultValue: String + + description: String + name: String! + type: __Type! + } + + """ + 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 directives supported by this server. + """ + directives: [__Directive!]! + + """ + If this server supports mutation, the type that mutation operations will be rooted at. + """ + mutationType: __Type + + """ + The type that query operations will be rooted at. + """ + queryType: __Type! + + """ + If this server support subscription, the type that subscription operations will be rooted at. + """ + subscriptionType: __Type + + """ + A list of all types supported by this server. + """ + types: [__Type!]! + } + + """ + 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 { + description: String + enumValues(includeDeprecated: Boolean = false): [__EnumValue!] + fields(includeDeprecated: Boolean = false): [__Field!] + inputFields: [__InputValue!] + interfaces: [__Type!] + kind: __TypeKind! + name: String + ofType: __Type + possibleTypes: [__Type!] + } + + """ + 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 using the Markdown syntax (as specified by [CommonMark](https://commonmark.org/). + """ + reason: String = "No longer supported" + ) + on + | ENUM_VALUE + | FIELD_DEFINITION + + """ + 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 diff --git a/src/SchemaPrinter/IntrospectionPrinterTest~test-settings.graphql b/src/SchemaPrinter/IntrospectionPrinterTest~test-settings.graphql new file mode 100644 index 00000000..ce5c8af0 --- /dev/null +++ b/src/SchemaPrinter/IntrospectionPrinterTest~test-settings.graphql @@ -0,0 +1,281 @@ +""" +A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies. +""" +enum __DirectiveLocation { + """ + Location adjacent to an argument definition. + """ + ARGUMENT_DEFINITION + + """ + Location adjacent to an enum definition. + """ + ENUM + + """ + Location adjacent to an enum value definition. + """ + ENUM_VALUE + + """ + Location adjacent to a field. + """ + FIELD + + """ + Location adjacent to a field definition. + """ + FIELD_DEFINITION + + """ + 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 an input object field definition. + """ + INPUT_FIELD_DEFINITION + + """ + Location adjacent to an input object type definition. + """ + INPUT_OBJECT + + """ + Location adjacent to an interface definition. + """ + INTERFACE + + """ + Location adjacent to a mutation operation. + """ + MUTATION + + """ + Location adjacent to an object type definition. + """ + OBJECT + + """ + Location adjacent to a query operation. + """ + QUERY + + """ + Location adjacent to a scalar definition. + """ + SCALAR + + """ + Location adjacent to a schema definition. + """ + SCHEMA + + """ + Location adjacent to a subscription operation. + """ + SUBSCRIPTION + + """ + Location adjacent to a union definition. + """ + UNION + + """ + Location adjacent to a variable definition. + """ + VARIABLE_DEFINITION +} + +""" +An enum describing what kind of type a given `__Type` is. +""" +enum __TypeKind { + """ + 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 an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields. + """ + INTERFACE + + """ + 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 + + """ + Indicates this type is an object. `fields` and `interfaces` are valid fields. + """ + OBJECT + + """ + Indicates this type is a scalar. + """ + SCALAR + + """ + Indicates this type is a union. `possibleTypes` is a valid field. + """ + UNION +} + +""" +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 { + args: [__InputValue!]! + description: String + isRepeatable: Boolean! + locations: [__DirectiveLocation!]! + name: String! +} + +""" +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 { + deprecationReason: String + description: String + isDeprecated: Boolean! + name: 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 { + args: [__InputValue!]! + deprecationReason: String + description: String + isDeprecated: Boolean! + name: String! + type: __Type! +} + +""" +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 { + """ + A GraphQL-formatted string representing the default value for this input value. + """ + defaultValue: String + + description: String + name: String! + type: __Type! +} + +""" +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 directives supported by this server. + """ + directives: [__Directive!]! + + """ + If this server supports mutation, the type that mutation operations will be rooted at. + """ + mutationType: __Type + + """ + The type that query operations will be rooted at. + """ + queryType: __Type! + + """ + If this server support subscription, the type that subscription operations will be rooted at. + """ + subscriptionType: __Type + + """ + A list of all types supported by this server. + """ + types: [__Type!]! +} + +""" +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 { + description: String + enumValues(includeDeprecated: Boolean = false): [__EnumValue!] + fields(includeDeprecated: Boolean = false): [__Field!] + inputFields: [__InputValue!] + interfaces: [__Type!] + kind: __TypeKind! + name: String + ofType: __Type + possibleTypes: [__Type!] +} + +""" +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 using the Markdown syntax (as specified by [CommonMark](https://commonmark.org/). + """ + reason: String = "No longer supported" +) +on + | ENUM_VALUE + | FIELD_DEFINITION + +""" +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 diff --git a/src/SchemaPrinter/Misc/DirectiveResolver.php b/src/SchemaPrinter/Misc/DirectiveResolver.php new file mode 100644 index 00000000..9df0de70 --- /dev/null +++ b/src/SchemaPrinter/Misc/DirectiveResolver.php @@ -0,0 +1,77 @@ + + */ + protected array $directives; + + /** + * @param array $directives + */ + public function __construct( + protected DirectiveLocator $locator, + protected ExecutableTypeNodeConverter $converter, + array $directives = [], + ) { + $this->factory = new DirectiveFactory($this->converter); + $this->directives = []; + + foreach ($directives as $directive) { + $this->directives[$directive->name] = $directive; + } + } + + public function getDefinition(string $name): GraphQLDirective { + $directive = $this->directives[$name] ?? null; + + if (!$directive) { + $definition = $this->locator->resolve($name)::definition(); + $definition = ASTHelper::extractDirectiveDefinition($definition); + $directive = $this->factory->handle($definition); + } + + return $directive; + } + + public function getInstance(string $name): GraphQLDirective|LighthouseDirective { + return $this->directives[$name] ?? $this->locator->create($name); + } + + /** + * @return array + */ + public function getDefinitions(): array { + $directives = $this->directives; + + foreach ($this->locator->definitions() as $definition) { + $directive = $this->factory->handle($definition); + $directives[$directive->name] = $directive; + } + + return $directives; + } +} diff --git a/src/SchemaPrinter/Misc/PrinterSettings.php b/src/SchemaPrinter/Misc/PrinterSettings.php new file mode 100644 index 00000000..f303ce12 --- /dev/null +++ b/src/SchemaPrinter/Misc/PrinterSettings.php @@ -0,0 +1,119 @@ + + // ========================================================================= + public function getResolver(): DirectiveResolver { + return $this->resolver; + } + // + + // + // ========================================================================= + public function getDirective(DirectiveNode $node): GraphQLDirective|LighthouseDirective { + return $this->resolver->getInstance($node->name->value); + } + // + + // + // ========================================================================= + public function getSpace(): string { + return $this->settings->getSpace(); + } + + public function getIndent(): string { + return $this->settings->getIndent(); + } + + public function getFileEnd(): string { + return $this->settings->getFileEnd(); + } + + public function getLineEnd(): string { + return $this->settings->getLineEnd(); + } + + public function getLineLength(): int { + return $this->settings->getLineLength(); + } + + public function isPrintDirectives(): bool { + return $this->settings->isPrintDirectives(); + } + + public function isPrintDirectiveDefinitions(): bool { + return $this->settings->isPrintDirectiveDefinitions(); + } + + public function isPrintDirectivesInDescription(): bool { + return $this->settings->isPrintDirectivesInDescription(); + } + + public function isPrintUnusedDefinitions(): bool { + return $this->settings->isPrintUnusedDefinitions(); + } + + public function isNormalizeSchema(): bool { + return $this->settings->isNormalizeSchema(); + } + + public function isNormalizeUnions(): bool { + return $this->settings->isNormalizeUnions(); + } + + public function isNormalizeEnums(): bool { + return $this->settings->isNormalizeEnums(); + } + + public function isNormalizeInterfaces(): bool { + return $this->settings->isNormalizeInterfaces(); + } + + public function isNormalizeFields(): bool { + return $this->settings->isNormalizeFields(); + } + + public function isNormalizeArguments(): bool { + return $this->settings->isNormalizeArguments(); + } + + public function isNormalizeDescription(): bool { + return $this->settings->isNormalizeDescription(); + } + + public function isNormalizeDirectiveLocations(): bool { + return $this->settings->isNormalizeDirectiveLocations(); + } + + public function isAlwaysMultilineUnions(): bool { + return $this->settings->isAlwaysMultilineUnions(); + } + + public function isAlwaysMultilineInterfaces(): bool { + return $this->settings->isAlwaysMultilineInterfaces(); + } + + public function isAlwaysMultilineDirectiveLocations(): bool { + return $this->settings->isAlwaysMultilineDirectiveLocations(); + } + + public function getDirectiveFilter(): ?DirectiveFilter { + return $this->settings->getDirectiveFilter(); + } + // +} diff --git a/src/SchemaPrinter/PrintedSchema.php b/src/SchemaPrinter/PrintedSchema.php new file mode 100644 index 00000000..9674917f --- /dev/null +++ b/src/SchemaPrinter/PrintedSchema.php @@ -0,0 +1,33 @@ +schema; + } + + /** + * @inheritDoc + */ + public function getUsedTypes(): array { + return $this->schema->getUsedTypes(); + } + + /** + * @inheritDoc + */ + public function getUsedDirectives(): array { + return $this->schema->getUsedDirectives(); + } +} diff --git a/src/SchemaPrinter/Printer.php b/src/SchemaPrinter/Printer.php new file mode 100644 index 00000000..7c6eb129 --- /dev/null +++ b/src/SchemaPrinter/Printer.php @@ -0,0 +1,208 @@ +setSettings($settings); + } + + public function getLevel(): int { + return $this->level; + } + + public function setLevel(int $level): static { + $this->level = $level; + + return $this; + } + + public function getSettings(): Settings { + return $this->settings; + } + + public function setSettings(?Settings $settings): static { + $this->settings = $settings ?? new DefaultSettings(); + + return $this; + } + + public function print(Schema $schema): PrintedSchema { + // todo(graphql): directives in description for schema + // https://github.com/webonyx/graphql-php/issues/1027 + + // Print + $resolver = new DirectiveResolver($this->locator, $this->converter, $schema->getDirectives()); + $settings = new PrinterSettings($resolver, $this->getSettings()); + $block = $this->getSchemaDefinition($settings, $schema); + $content = $this->getDefinitionList($settings, true); + $content[] = $block; + + if ($settings->isPrintUnusedDefinitions()) { + $content[] = $this->getTypeDefinitions($settings, $schema); + $content[] = $this->getDirectiveDefinitions($settings, $schema); + } else { + foreach ($this->getUsedDefinitions($settings, $schema, $block) as $definition) { + $content[] = $definition; + } + } + + // Return + return new PrintedSchema($content); + } + + protected function getSchemaDefinition(PrinterSettings $settings, Schema $schema): Block { + return $this->getDefinitionBlock($settings, $schema); + } + + /** + * Returns all types defined in the schema. + * + * @return BlockList + */ + protected function getTypeDefinitions(PrinterSettings $settings, Schema $schema): BlockList { + $blocks = $this->getDefinitionList($settings); + + foreach ($schema->getTypeMap() as $type) { + // Standard? + if (!$this->isType($type)) { + continue; + } + + // Nope + $blocks[] = $this->getDefinitionBlock($settings, $type); + } + + return $blocks; + } + + /** + * Returns all directives defined in the schema. + * + * @return BlockList + */ + protected function getDirectiveDefinitions(PrinterSettings $settings, Schema $schema): BlockList { + // Included? + $blocks = $this->getDefinitionList($settings); + + if ($settings->isPrintDirectiveDefinitions()) { + $filter = $settings->getDirectiveFilter(); + $directives = $settings->getResolver()->getDefinitions(); + + foreach ($directives as $directive) { + // Introspection? + if (!$this->isDirective($directive)) { + continue; + } + + // Not allowed? + if ($filter !== null && !$filter->isAllowedDirective($directive)) { + continue; + } + + // Nope + $blocks[] = $this->getDefinitionBlock($settings, $directive); + } + } + + // Return + return $blocks; + } + + /** + * @return array + */ + protected function getUsedDefinitions(PrinterSettings $settings, Schema $schema, Block $root): array { + $directivesDefinitions = $settings->isPrintDirectiveDefinitions(); + $directivesResolver = $settings->getResolver(); + $directives = $this->getDefinitionList($settings); + $types = $this->getDefinitionList($settings); + $stack = $root->getUsedDirectives() + $root->getUsedTypes(); + + while ($stack) { + // Added? + $name = array_pop($stack); + + if (isset($types[$name]) || isset($directives[$name])) { + continue; + } + + // Add + $block = null; + + if (str_starts_with($name, '@')) { + if ($directivesDefinitions) { + $directive = $directivesResolver->getDefinition(substr($name, 1)); + + if ($this->isDirective($directive)) { + $block = $this->getDefinitionBlock($settings, $directive); + $directives[$name] = $block; + } + } + } else { + $type = $schema->getType($name); + + if ($type && $this->isType($type)) { + $block = $this->getDefinitionBlock($settings, $type); + $types[$name] = $block; + } + } + + // Stack + if ($block) { + $stack = $stack + + $block->getUsedDirectives() + + $block->getUsedTypes(); + } + } + + return [ + $types, + $directives, + ]; + } + + protected function getDefinitionList(PrinterSettings $settings, bool $schema = false): BlockList { + return new DefinitionList($settings, $this->getLevel(), $schema); + } + + protected function getDefinitionBlock( + PrinterSettings $settings, + Schema|Type|Directive $definition, + ): Block { + return new DefinitionBlock($settings, $this->getLevel(), $definition); + } + + private function isType(?Type $type): bool { + return $type && !Type::isBuiltInType($type); + } + + private function isDirective(?Directive $directive): bool { + return $directive && !Directive::isSpecifiedDirective($directive); + } +} diff --git a/src/SchemaPrinter/PrinterTest.php b/src/SchemaPrinter/PrinterTest.php new file mode 100644 index 00000000..db957f1a --- /dev/null +++ b/src/SchemaPrinter/PrinterTest.php @@ -0,0 +1,342 @@ + + // ========================================================================= + /** + * @covers ::print + * + * @dataProvider dataProviderPrint + * + * @param array{schema: string, types: array, directives: array} $expected + */ + public function testPrint(array $expected, ?Settings $settings, int $level): void { + // Types + $directives = $this->app->make(DirectiveLocator::class); + $registry = $this->app->make(TypeRegistry::class); + $directive = (new class() extends BaseDirective { + public static function definition(): string { + throw new Exception('Should not be called.'); + } + })::class; + + $codeScalar = new StringType([ + 'name' => 'CodeScalar', + ]); + $codeEnum = new EnumType([ + 'name' => 'CodeEnum', + 'values' => ['C', 'B', 'A'], + ]); + $codeInterface = new InterfaceType([ + 'name' => 'CodeInterface', + 'astNode' => Parser::interfaceTypeDefinition('interface CodeInterface @codeDirective'), + 'description' => 'Description', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::nonNull(Type::boolean()), + ], + ], + ]); + $codeType = new ObjectType([ + 'name' => 'CodeType', + 'astNode' => Parser::objectTypeDefinition('type CodeType @schemaDirective'), + 'description' => 'Description', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::boolean(), + ], + ], + ]); + $codeUnion = new UnionType([ + 'name' => 'CodeUnion', + 'types' => [ + $codeType, + ], + ]); + $codeInput = new InputObjectType([ + 'name' => 'CodeInput', + 'astNode' => Parser::inputObjectTypeDefinition('input InputObjectType @schemaDirective'), + 'description' => 'Description', + 'fields' => [ + [ + 'name' => 'a', + 'type' => Type::boolean(), + ], + ], + ]); + + $directives->setResolved('schemaDirective', $directive); + $directives->setResolved('schemaDirectiveUnused', $directive); + $directives->setResolved( + 'codeDirective', + (new class() extends BaseDirective { + public static function definition(): string { + return 'directive @codeDirective repeatable on SCHEMA | SCALAR | INTERFACE'; + } + })::class, + ); + $registry->register($codeScalar); + $registry->register($codeEnum); + $registry->register($codeInterface); + $registry->register($codeType); + $registry->register($codeUnion); + $registry->register($codeInput); + + // Test + $output = $this->getTestData()->content($expected['schema']); + $printer = $this->app->make(Printer::class)->setSettings($settings)->setLevel($level); + $schema = $this->getGraphQLSchema($this->getTestData()->file('~schema.graphql')); + $actual = $printer->print($schema); + + self::assertEquals($output, (string) $actual); + self::assertEqualsCanonicalizing($expected['types'], array_values($actual->getUsedTypes())); + self::assertEqualsCanonicalizing($expected['directives'], array_values($actual->getUsedDirectives())); + } + // + + // + // ========================================================================= + /** + * @return array> + */ + public function dataProviderPrint(): array { + return [ + 'null' => [ + [ + 'schema' => '~default-settings.graphql', + 'types' => [ + 'String', + 'Boolean', + 'SchemaType', + 'SchemaEnum', + 'SchemaInput', + 'SchemaUnion', + 'SchemaScalar', + 'SchemaInterfaceB', + 'CodeScalar', + 'CodeInput', + 'CodeUnion', + 'CodeEnum', + 'CodeType', + ], + 'directives' => [ + '@deprecated', + ], + ], + null, + 0, + ], + DefaultSettings::class => [ + [ + 'schema' => '~default-settings.graphql', + 'types' => [ + 'String', + 'Boolean', + 'SchemaType', + 'SchemaEnum', + 'SchemaInput', + 'SchemaUnion', + 'SchemaScalar', + 'SchemaInterfaceB', + 'CodeScalar', + 'CodeInput', + 'CodeUnion', + 'CodeEnum', + 'CodeType', + ], + 'directives' => [ + '@deprecated', + ], + ], + new DefaultSettings(), + 0, + ], + GraphQLSettings::class => [ + [ + 'schema' => '~graphql-settings.graphql', + 'types' => [ + 'String', + 'Boolean', + 'SchemaType', + 'SchemaEnum', + 'SchemaInput', + 'SchemaUnion', + 'SchemaScalar', + 'SchemaInterfaceB', + 'CodeScalar', + 'CodeInput', + 'CodeUnion', + 'CodeEnum', + 'CodeType', + 'SchemaTypeUnused', + 'SchemaEnumUnused', + 'SchemaScalarUnused', + ], + 'directives' => [ + '@deprecated', + ], + ], + new GraphQLSettings(), + 0, + ], + TestSettings::class => [ + [ + 'schema' => '~test-settings.graphql', + 'types' => [ + 'String', + 'Boolean', + 'SchemaType', + 'SchemaEnum', + 'SchemaInput', + 'SchemaUnion', + 'SchemaScalar', + 'SchemaInterfaceB', + 'CodeScalar', + 'CodeInput', + 'CodeUnion', + 'CodeEnum', + 'CodeType', + ], + 'directives' => [ + '@schemaDirective', + '@codeDirective', + '@deprecated', + '@scalar', + '@mock', + ], + ], + new TestSettings(), + 0, + ], + TestSettings::class.' (no directives definitions)' => [ + [ + 'schema' => '~test-settings-no-directives-definitions.graphql', + 'types' => [ + 'String', + 'Boolean', + 'SchemaType', + 'SchemaEnum', + 'SchemaInput', + 'SchemaUnion', + 'SchemaScalar', + 'SchemaInterfaceB', + 'CodeScalar', + 'CodeInput', + 'CodeUnion', + 'CodeEnum', + 'CodeType', + ], + 'directives' => [ + '@schemaDirective', + '@codeDirective', + '@deprecated', + '@scalar', + '@mock', + ], + ], + (new TestSettings()) + ->setPrintDirectiveDefinitions(false), + 0, + ], + TestSettings::class.' (directives in description)' => [ + [ + 'schema' => '~test-settings-directives-in-description.graphql', + 'types' => [ + 'String', + 'Boolean', + 'SchemaType', + 'SchemaEnum', + 'SchemaInput', + 'SchemaUnion', + 'SchemaScalar', + 'SchemaInterfaceB', + 'CodeScalar', + 'CodeInput', + 'CodeUnion', + 'CodeEnum', + 'CodeType', + ], + 'directives' => [ + // empty + ], + ], + (new TestSettings()) + ->setPrintDirectives(false) + ->setPrintDirectiveDefinitions(false) + ->setPrintDirectivesInDescription(true), + 0, + ], + TestSettings::class.' (no normalization)' => [ + [ + 'schema' => '~test-settings-no-normalization.graphql', + 'types' => [ + 'String', + 'Boolean', + 'SchemaType', + 'SchemaEnum', + 'SchemaInput', + 'SchemaUnion', + 'SchemaScalar', + 'SchemaInterfaceB', + 'CodeScalar', + 'CodeInput', + 'CodeUnion', + 'CodeEnum', + 'CodeType', + ], + 'directives' => [ + '@schemaDirective', + '@codeDirective', + '@deprecated', + '@scalar', + '@mock', + ], + ], + (new TestSettings()) + ->setNormalizeSchema(false) + ->setNormalizeUnions(false) + ->setNormalizeEnums(false) + ->setNormalizeInterfaces(false) + ->setNormalizeFields(false) + ->setNormalizeArguments(false) + ->setNormalizeDescription(false) + ->setNormalizeDirectiveLocations(false) + ->setAlwaysMultilineUnions(false) + ->setAlwaysMultilineInterfaces(false) + ->setAlwaysMultilineDirectiveLocations(false), + 0, + ], + + // Settings + ]; + } + // +} diff --git a/src/SchemaPrinter/PrinterTest~default-settings.graphql b/src/SchemaPrinter/PrinterTest~default-settings.graphql new file mode 100644 index 00000000..b090a8c7 --- /dev/null +++ b/src/SchemaPrinter/PrinterTest~default-settings.graphql @@ -0,0 +1,160 @@ +enum CodeEnum { + A + B + C +} + +enum SchemaEnum { + A + @deprecated + + """ + Description + """ + B +} + +""" +Description +""" +input CodeInput { + a: Boolean +} + +input SchemaInput { + a: CodeScalar + b: CodeEnum + c: SchemaScalar + d: SchemaEnum + + """ + Recursion + """ + e: SchemaInput + + f: [String!] +} + +""" +Lighthouse not yet support "implements" for interface... + +@see https://github.com/webonyx/graphql-php/issues/728 +""" +interface SchemaInterfaceB { + a: Boolean! + + """ + Deprecated field + """ + b: [String]! + @deprecated + + c( + """ + aaa + """ + a: String + + """ + bbb + """ + b: [SchemaScalar!]! + + c: SchemaEnum + ): CodeUnion + + d: CodeScalar + e: CodeEnum +} + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar CodeScalar + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar SchemaScalar + +""" +Description +""" +type CodeType { + a: Boolean +} + +type Query { + a: SchemaType + @deprecated(reason: "deprecated reason") + + b: SchemaEnum + @deprecated + + c( + a: SchemaInput = { + a: "aaa" + b: A + c: "ccc" + d: A + e: { + a: "aaa" + b: A + c: "ccc" + d: A + f: ["aaa", "bbb", "ccc", "ddd"] + } + } + ): CodeScalar + + d(a: SchemaInput = {}): CodeType +} + +type SchemaType +implements + & SchemaInterfaceB +{ + a: Boolean! + + """ + Deprecated field + """ + b: [String]! + @deprecated + + c( + """ + aaa + """ + a: String + + """ + bbb + """ + b: [SchemaScalar!]! + + c: CodeInput + ): CodeUnion + + d: CodeScalar + e: CodeEnum + + f( + a: [String!] = [ + "very very very long line of text" + "very very very long line of text" + "very very very long line of text" + ] + ): SchemaUnion +} + +union CodeUnion = + | CodeType + +union SchemaUnion = + | CodeType + | SchemaType diff --git a/src/SchemaPrinter/PrinterTest~graphql-settings.graphql b/src/SchemaPrinter/PrinterTest~graphql-settings.graphql new file mode 100644 index 00000000..8264b035 --- /dev/null +++ b/src/SchemaPrinter/PrinterTest~graphql-settings.graphql @@ -0,0 +1,211 @@ +enum CodeEnum { + C + B + A +} + +enum SchemaEnum { + A + @deprecated + + """ + Description + """ + B +} + +""" +This is unused enum. +""" +enum SchemaEnumUnused { + A +} + +""" +Description +""" +input CodeInput { + a: Boolean +} + +input SchemaInput { + f: [String!] + + """ + Recursion + """ + e: SchemaInput + + d: SchemaEnum + c: SchemaScalar + b: CodeEnum + a: CodeScalar +} + +""" +This is unused input. +""" +input SchemaInputUnused { + a: CodeScalar + b: CodeEnum + c: SchemaScalar + d: SchemaEnum + + """ + Recursion + """ + e: SchemaInput +} + +""" +Description +""" +interface CodeInterface { + a: Boolean! +} + +interface SchemaInterfaceA { + a: Boolean! +} + +""" +Lighthouse not yet support "implements" for interface... + +@see https://github.com/webonyx/graphql-php/issues/728 +""" +interface SchemaInterfaceB { + a: Boolean! + + """ + Deprecated field + """ + b: [String]! + @deprecated + + c( + """ + aaa + """ + a: String + + """ + bbb + """ + b: [SchemaScalar!]! + + c: SchemaEnum + ): CodeUnion + + d: CodeScalar + e: CodeEnum +} + +""" +This is unused interface. +""" +interface SchemaInterfaceUnused { + a: SchemaScalarUnused + b: SchemaEnumUnused +} + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar CodeScalar + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar SchemaScalar + +""" +This is unused scalar. +""" +scalar SchemaScalarUnused + +""" +Description +""" +type CodeType { + a: Boolean +} + +type Query { + a: SchemaType + @deprecated(reason: "deprecated reason") + + b: SchemaEnum + @deprecated + + c( + a: SchemaInput = { + e: { + f: ["aaa", "bbb", "ccc", "ddd"] + d: A + c: "ccc" + b: A + a: "aaa" + } + d: A + c: "ccc" + b: A + a: "aaa" + } + ): CodeScalar + + d(a: SchemaInput = {}): CodeType +} + +type SchemaType implements SchemaInterfaceB { + a: Boolean! + + """ + Deprecated field + """ + b: [String]! + @deprecated + + c( + """ + aaa + """ + a: String + + """ + bbb + """ + b: [SchemaScalar!]! + + c: CodeInput + ): CodeUnion + + d: CodeScalar + e: CodeEnum + + f( + a: [String!] = [ + "very very very long line of text" + "very very very long line of text" + "very very very long line of text" + ] + ): SchemaUnion +} + +""" +This is unused type. +""" +type SchemaTypeUnused { + a: SchemaScalarUnused +} + +union CodeUnion = CodeType +union SchemaUnion = SchemaType | CodeType + +""" +This is unused union. +""" +union SchemaUnionUnused = SchemaTypeUnused diff --git a/src/SchemaPrinter/PrinterTest~schema.graphql b/src/SchemaPrinter/PrinterTest~schema.graphql new file mode 100644 index 00000000..dc44e0e5 --- /dev/null +++ b/src/SchemaPrinter/PrinterTest~schema.graphql @@ -0,0 +1,140 @@ +"""Directive""" +directive @schemaDirective( + """ + + Directive argument + + """ + message: String +) on SCHEMA | FIELD | ARGUMENT_DEFINITION | INTERFACE | OBJECT | UNION | INPUT_OBJECT | SCALAR + +scalar SchemaScalar @scalar(class: "GraphQL\\Type\\Definition\\StringType") @codeDirective + +enum SchemaEnum { + A @deprecated + + "Description" + B +} + +interface SchemaInterfaceA { + a: Boolean! +} + +""" +Lighthouse not yet support "implements" for interface... + +@see https://github.com/webonyx/graphql-php/issues/728 +""" +interface SchemaInterfaceB implements SchemaInterfaceA & CodeInterface @schemaDirective { + a: Boolean! + "Deprecated field" + b: [String]! @deprecated + c( + "aaa" + a: String + "bbb" + b: [SchemaScalar!]! + c: SchemaEnum + ): CodeUnion + d: CodeScalar + e: CodeEnum +} + +type Query { + a: SchemaType @deprecated(reason: "deprecated reason") @codeDirective @mock + b: SchemaEnum @deprecated(reason: "No longer supported") @mock + c (a: SchemaInput = {a: "aaa", b: A, c: "ccc", d: A, e: {a: "aaa", b: A, c: "ccc", d: A, f: ["aaa", "bbb", "ccc", "ddd"]}}): CodeScalar @mock + d (a: SchemaInput = {}): CodeType @mock +} + +type SchemaType implements SchemaInterfaceB @schemaDirective { + a: Boolean! + "Deprecated field" + b: [String]! @deprecated + c( + "aaa" + a: String + "bbb" + b: [SchemaScalar!]! + c: CodeInput + ): CodeUnion + d: CodeScalar + e: CodeEnum + f(a: [String!] = ["very very very long line of text", "very very very long line of text", "very very very long line of text"]): SchemaUnion +} + +union SchemaUnion @schemaDirective = SchemaType | CodeType + +input SchemaInput @schemaDirective { + f: [String!] + + "Recursion" + e: SchemaInput + d: SchemaEnum + + """ + + """ + c: SchemaScalar + + + b: CodeEnum + a: CodeScalar +} + +# Unused + +""" +This is unused directives. +""" +directive @schemaDirectiveUnused(a: SchemaScalarUnused, b: SchemaEnumUnused) repeatable on SCALAR | OBJECT + +""" +This is unused scalar. +""" +scalar SchemaScalarUnused @scalar(class: "GraphQL\\Type\\Definition\\StringType") + + +""" +This is unused enum. +""" +enum SchemaEnumUnused { + A +} + +""" +This is unused interface. +""" +interface SchemaInterfaceUnused { + a: SchemaScalarUnused + b: SchemaEnumUnused +} + +""" +This is unused type. +""" +type SchemaTypeUnused @schemaDirectiveUnused { + a: SchemaScalarUnused +} + +""" +This is unused union. +""" +union SchemaUnionUnused = SchemaTypeUnused + +""" +This is unused input. +""" +input SchemaInputUnused @schemaDirective { + a: CodeScalar + b: CodeEnum + """ + + """ + c: SchemaScalar + d: SchemaEnum + + "Recursion" + e: SchemaInput +} diff --git a/src/SchemaPrinter/PrinterTest~test-settings-directives-in-description.graphql b/src/SchemaPrinter/PrinterTest~test-settings-directives-in-description.graphql new file mode 100644 index 00000000..facc0ed9 --- /dev/null +++ b/src/SchemaPrinter/PrinterTest~test-settings-directives-in-description.graphql @@ -0,0 +1,195 @@ +enum CodeEnum { + A + B + C +} + +enum SchemaEnum { + """ + @deprecated + """ + A + + """ + Description + """ + B +} + +""" +Description + +@schemaDirective +""" +input CodeInput { + a: Boolean +} + +""" +@schemaDirective +""" +input SchemaInput { + a: CodeScalar + b: CodeEnum + c: SchemaScalar + d: SchemaEnum + + """ + Recursion + """ + e: SchemaInput + + f: [String!] +} + +""" +Lighthouse not yet support "implements" for interface... + +@see https://github.com/webonyx/graphql-php/issues/728 + +@schemaDirective +""" +interface SchemaInterfaceB { + a: Boolean! + + """ + Deprecated field + + @deprecated + """ + b: [String]! + + c( + """ + aaa + """ + a: String + + """ + bbb + """ + b: [SchemaScalar!]! + + c: SchemaEnum + ): CodeUnion + + d: CodeScalar + e: CodeEnum +} + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar CodeScalar + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. + +@scalar(class: "GraphQL\\Type\\Definition\\StringType") +@codeDirective +""" +scalar SchemaScalar + +""" +Description + +@schemaDirective +""" +type CodeType { + a: Boolean +} + +type Query { + """ + @deprecated(reason: "deprecated reason") + @codeDirective + @mock + """ + a: SchemaType + + """ + @deprecated + @mock + """ + b: SchemaEnum + + """ + @mock + """ + c( + a: SchemaInput = { + a: "aaa" + b: A + c: "ccc" + d: A + e: { + a: "aaa" + b: A + c: "ccc" + d: A + f: ["aaa", "bbb", "ccc", "ddd"] + } + } + ): CodeScalar + + """ + @mock + """ + d(a: SchemaInput = {}): CodeType +} + +""" +@schemaDirective +""" +type SchemaType +implements + & SchemaInterfaceB +{ + a: Boolean! + + """ + Deprecated field + + @deprecated + """ + b: [String]! + + c( + """ + aaa + """ + a: String + + """ + bbb + """ + b: [SchemaScalar!]! + + c: CodeInput + ): CodeUnion + + d: CodeScalar + e: CodeEnum + + f( + a: [String!] = [ + "very very very long line of text" + "very very very long line of text" + "very very very long line of text" + ] + ): SchemaUnion +} + +union CodeUnion = + | CodeType + +""" +@schemaDirective +""" +union SchemaUnion = + | CodeType + | SchemaType diff --git a/src/SchemaPrinter/PrinterTest~test-settings-no-directives-definitions.graphql b/src/SchemaPrinter/PrinterTest~test-settings-no-directives-definitions.graphql new file mode 100644 index 00000000..392a9406 --- /dev/null +++ b/src/SchemaPrinter/PrinterTest~test-settings-no-directives-definitions.graphql @@ -0,0 +1,178 @@ +enum CodeEnum { + A + B + C +} + +enum SchemaEnum { + A + @deprecated + + """ + Description + """ + B +} + +""" +Description +""" +input CodeInput +@schemaDirective +{ + a: Boolean +} + +input SchemaInput +@schemaDirective +{ + a: CodeScalar + b: CodeEnum + c: SchemaScalar + d: SchemaEnum + + """ + Recursion + """ + e: SchemaInput + + f: [String!] +} + +""" +Lighthouse not yet support "implements" for interface... + +@see https://github.com/webonyx/graphql-php/issues/728 +""" +interface SchemaInterfaceB +@schemaDirective +{ + a: Boolean! + + """ + Deprecated field + """ + b: [String]! + @deprecated + + c( + """ + aaa + """ + a: String + + """ + bbb + """ + b: [SchemaScalar!]! + + c: SchemaEnum + ): CodeUnion + + d: CodeScalar + e: CodeEnum +} + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar CodeScalar + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar SchemaScalar +@scalar(class: "GraphQL\\Type\\Definition\\StringType") +@codeDirective + +""" +Description +""" +type CodeType +@schemaDirective +{ + a: Boolean +} + +type Query { + a: SchemaType + @deprecated(reason: "deprecated reason") + @codeDirective + @mock + + b: SchemaEnum + @deprecated + @mock + + c( + a: SchemaInput = { + a: "aaa" + b: A + c: "ccc" + d: A + e: { + a: "aaa" + b: A + c: "ccc" + d: A + f: ["aaa", "bbb", "ccc", "ddd"] + } + } + ): CodeScalar + @mock + + d(a: SchemaInput = {}): CodeType + @mock +} + +type SchemaType +implements + & SchemaInterfaceB +@schemaDirective +{ + a: Boolean! + + """ + Deprecated field + """ + b: [String]! + @deprecated + + c( + """ + aaa + """ + a: String + + """ + bbb + """ + b: [SchemaScalar!]! + + c: CodeInput + ): CodeUnion + + d: CodeScalar + e: CodeEnum + + f( + a: [String!] = [ + "very very very long line of text" + "very very very long line of text" + "very very very long line of text" + ] + ): SchemaUnion +} + +union CodeUnion = + | CodeType + +union SchemaUnion +@schemaDirective += + | CodeType + | SchemaType diff --git a/src/SchemaPrinter/PrinterTest~test-settings-no-normalization.graphql b/src/SchemaPrinter/PrinterTest~test-settings-no-normalization.graphql new file mode 100644 index 00000000..377b6b03 --- /dev/null +++ b/src/SchemaPrinter/PrinterTest~test-settings-no-normalization.graphql @@ -0,0 +1,216 @@ +type Query { + a: SchemaType + @deprecated(reason: "deprecated reason") + @codeDirective + @mock + + b: SchemaEnum + @deprecated + @mock + + c( + a: SchemaInput = { + e: { + f: ["aaa", "bbb", "ccc", "ddd"] + d: A + c: "ccc" + b: A + a: "aaa" + } + d: A + c: "ccc" + b: A + a: "aaa" + } + ): CodeScalar + @mock + + d(a: SchemaInput = {}): CodeType + @mock +} + +""" +Description +""" +type CodeType +@schemaDirective +{ + a: Boolean +} + +input SchemaInput +@schemaDirective +{ + f: [String!] + + """ + Recursion + """ + e: SchemaInput + + d: SchemaEnum + c: SchemaScalar + b: CodeEnum + a: CodeScalar +} + +enum CodeEnum { + C + B + A +} + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar SchemaScalar +@scalar(class: "GraphQL\\Type\\Definition\\StringType") +@codeDirective + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar CodeScalar + +enum SchemaEnum { + A + @deprecated + + """ + Description + """ + B +} + +type SchemaType implements SchemaInterfaceB +@schemaDirective +{ + a: Boolean! + + """ + Deprecated field + """ + b: [String]! + @deprecated + + c( + """ + aaa + """ + a: String + + """ + bbb + """ + b: [SchemaScalar!]! + + c: CodeInput + ): CodeUnion + + d: CodeScalar + e: CodeEnum + + f( + a: [String!] = [ + "very very very long line of text" + "very very very long line of text" + "very very very long line of text" + ] + ): SchemaUnion +} + +union SchemaUnion +@schemaDirective += SchemaType | CodeType + +""" +Description +""" +input CodeInput +@schemaDirective +{ + a: Boolean +} + +union CodeUnion = CodeType + +""" +Lighthouse not yet support "implements" for interface... + +@see https://github.com/webonyx/graphql-php/issues/728 +""" +interface SchemaInterfaceB +@schemaDirective +{ + a: Boolean! + + """ + Deprecated field + """ + b: [String]! + @deprecated + + c( + """ + aaa + """ + a: String + + """ + bbb + """ + b: [SchemaScalar!]! + + c: SchemaEnum + ): CodeUnion + + d: CodeScalar + e: CodeEnum +} + +""" +Directive +""" +directive @schemaDirective( + """ + Directive argument + """ + message: String +) +on + | SCHEMA + | FIELD + | ARGUMENT_DEFINITION + | INTERFACE + | OBJECT + | UNION + | INPUT_OBJECT + | SCALAR + +""" +Reference a class implementing a scalar definition. +""" +directive @scalar( + """ + Reference to a class that extends `\GraphQL\Type\Definition\ScalarType`. + """ + class: String! +) +on | SCALAR + +""" +Allows you to easily hook up a resolver for an endpoint. +""" +directive @mock( + """ + Specify a unique key for the mock resolver. + """ + key: String = "default" +) +on | FIELD_DEFINITION + +directive @codeDirective repeatable on SCHEMA | SCALAR | INTERFACE diff --git a/src/SchemaPrinter/PrinterTest~test-settings.graphql b/src/SchemaPrinter/PrinterTest~test-settings.graphql new file mode 100644 index 00000000..271ee6aa --- /dev/null +++ b/src/SchemaPrinter/PrinterTest~test-settings.graphql @@ -0,0 +1,226 @@ +enum CodeEnum { + A + B + C +} + +enum SchemaEnum { + A + @deprecated + + """ + Description + """ + B +} + +""" +Description +""" +input CodeInput +@schemaDirective +{ + a: Boolean +} + +input SchemaInput +@schemaDirective +{ + a: CodeScalar + b: CodeEnum + c: SchemaScalar + d: SchemaEnum + + """ + Recursion + """ + e: SchemaInput + + f: [String!] +} + +""" +Lighthouse not yet support "implements" for interface... + +@see https://github.com/webonyx/graphql-php/issues/728 +""" +interface SchemaInterfaceB +@schemaDirective +{ + a: Boolean! + + """ + Deprecated field + """ + b: [String]! + @deprecated + + c( + """ + aaa + """ + a: String + + """ + bbb + """ + b: [SchemaScalar!]! + + c: SchemaEnum + ): CodeUnion + + d: CodeScalar + e: CodeEnum +} + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar CodeScalar + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar SchemaScalar +@scalar(class: "GraphQL\\Type\\Definition\\StringType") +@codeDirective + +""" +Description +""" +type CodeType +@schemaDirective +{ + a: Boolean +} + +type Query { + a: SchemaType + @deprecated(reason: "deprecated reason") + @codeDirective + @mock + + b: SchemaEnum + @deprecated + @mock + + c( + a: SchemaInput = { + a: "aaa" + b: A + c: "ccc" + d: A + e: { + a: "aaa" + b: A + c: "ccc" + d: A + f: ["aaa", "bbb", "ccc", "ddd"] + } + } + ): CodeScalar + @mock + + d(a: SchemaInput = {}): CodeType + @mock +} + +type SchemaType +implements + & SchemaInterfaceB +@schemaDirective +{ + a: Boolean! + + """ + Deprecated field + """ + b: [String]! + @deprecated + + c( + """ + aaa + """ + a: String + + """ + bbb + """ + b: [SchemaScalar!]! + + c: CodeInput + ): CodeUnion + + d: CodeScalar + e: CodeEnum + + f( + a: [String!] = [ + "very very very long line of text" + "very very very long line of text" + "very very very long line of text" + ] + ): SchemaUnion +} + +union CodeUnion = + | CodeType + +union SchemaUnion +@schemaDirective += + | CodeType + | SchemaType + +directive @codeDirective repeatable on + | INTERFACE + | SCALAR + | SCHEMA + +""" +Allows you to easily hook up a resolver for an endpoint. +""" +directive @mock( + """ + Specify a unique key for the mock resolver. + """ + key: String = "default" +) +on + | FIELD_DEFINITION + +""" +Reference a class implementing a scalar definition. +""" +directive @scalar( + """ + Reference to a class that extends `\GraphQL\Type\Definition\ScalarType`. + """ + class: String! +) +on + | SCALAR + +""" +Directive +""" +directive @schemaDirective( + """ + Directive argument + """ + message: String +) +on + | ARGUMENT_DEFINITION + | FIELD + | INPUT_OBJECT + | INTERFACE + | OBJECT + | SCALAR + | SCHEMA + | UNION diff --git a/src/SchemaPrinter/Settings/DefaultSettings.php b/src/SchemaPrinter/Settings/DefaultSettings.php new file mode 100644 index 00000000..280febca --- /dev/null +++ b/src/SchemaPrinter/Settings/DefaultSettings.php @@ -0,0 +1,29 @@ +name === Directive::DEPRECATED_NAME; + } +} diff --git a/src/SchemaPrinter/Settings/GraphQLSettings.php b/src/SchemaPrinter/Settings/GraphQLSettings.php new file mode 100644 index 00000000..d1bce16a --- /dev/null +++ b/src/SchemaPrinter/Settings/GraphQLSettings.php @@ -0,0 +1,38 @@ +directiveFilter = new GraphQLDirectiveFilter(); + } +} diff --git a/src/SchemaPrinter/Settings/ImmutableSettings.php b/src/SchemaPrinter/Settings/ImmutableSettings.php new file mode 100644 index 00000000..d1681c8a --- /dev/null +++ b/src/SchemaPrinter/Settings/ImmutableSettings.php @@ -0,0 +1,280 @@ +space; + } + + public function setSpace(string $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->space = $value; + }); + } + + public function getIndent(): string { + return $this->indent; + } + + public function setIndent(string $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->indent = $value; + }); + } + + public function getFileEnd(): string { + return $this->fileEnd; + } + + public function setFileEnd(string $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->fileEnd = $value; + }); + } + + public function getLineEnd(): string { + return $this->lineEnd; + } + + public function setLineEnd(string $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->lineEnd = $value; + }); + } + + public function getLineLength(): int { + return $this->lineLength; + } + + public function setLineLength(int $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->lineLength = $value; + }); + } + + public function isPrintUnusedDefinitions(): bool { + return $this->printUnusedDefinitions; + } + + public function setPrintUnusedDefinitions(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->printUnusedDefinitions = $value; + }); + } + + public function isPrintDirectives(): bool { + return $this->printDirectives; + } + + public function setPrintDirectives(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->printDirectives = $value; + }); + } + + public function isPrintDirectiveDefinitions(): bool { + return $this->printDirectiveDefinitions; + } + + public function setPrintDirectiveDefinitions(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->printDirectiveDefinitions = $value; + }); + } + + public function isPrintDirectivesInDescription(): bool { + return $this->printDirectivesInDescription; + } + + public function setPrintDirectivesInDescription(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->printDirectivesInDescription = $value; + }); + } + + public function isNormalizeSchema(): bool { + return $this->normalizeSchema; + } + + public function setNormalizeSchema(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->normalizeSchema = $value; + }); + } + + public function isNormalizeUnions(): bool { + return $this->normalizeUnions; + } + + public function setNormalizeUnions(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->normalizeUnions = $value; + }); + } + + public function isNormalizeEnums(): bool { + return $this->normalizeEnums; + } + + public function setNormalizeEnums(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->normalizeEnums = $value; + }); + } + + public function isNormalizeInterfaces(): bool { + return $this->normalizeInterfaces; + } + + public function setNormalizeInterfaces(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->normalizeInterfaces = $value; + }); + } + + public function isNormalizeFields(): bool { + return $this->normalizeFields; + } + + public function setNormalizeFields(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->normalizeFields = $value; + }); + } + + public function isNormalizeArguments(): bool { + return $this->normalizeArguments; + } + + public function setNormalizeArguments(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->normalizeArguments = $value; + }); + } + + public function isNormalizeDescription(): bool { + return $this->normalizeDescription; + } + + public function setNormalizeDescription(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->normalizeDescription = $value; + }); + } + + public function isNormalizeDirectiveLocations(): bool { + return $this->normalizeDirectiveLocations; + } + + public function setNormalizeDirectiveLocations(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->normalizeDirectiveLocations = $value; + }); + } + + public function isAlwaysMultilineUnions(): bool { + return $this->alwaysMultilineUnions; + } + + public function setAlwaysMultilineUnions(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->alwaysMultilineUnions = $value; + }); + } + + public function isAlwaysMultilineInterfaces(): bool { + return $this->alwaysMultilineInterfaces; + } + + public function setAlwaysMultilineInterfaces(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->alwaysMultilineInterfaces = $value; + }); + } + + public function isAlwaysMultilineDirectiveLocations(): bool { + return $this->alwaysMultilineDirectiveLocations; + } + + public function setAlwaysMultilineDirectiveLocations(bool $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->alwaysMultilineDirectiveLocations = $value; + }); + } + + public function getDirectiveFilter(): ?DirectiveFilter { + return $this->directiveFilter; + } + + public function setDirectiveFilter(?DirectiveFilter $value): static { + return $this->set(static function (self $settings) use ($value): void { + $settings->directiveFilter = $value; + }); + } + + protected function set(Closure $callback): static { + $settings = clone $this; + + $callback($settings); + + return $settings; + } + + public static function createFrom(Settings $settings): self { + return (new class() extends ImmutableSettings { + // empty + }) + ->setSpace($settings->getSpace()) + ->setIndent($settings->getIndent()) + ->setFileEnd($settings->getFileEnd()) + ->setLineEnd($settings->getLineEnd()) + ->setLineLength($settings->getLineLength()) + ->setPrintDirectives($settings->isPrintDirectives()) + ->setPrintDirectiveDefinitions($settings->isPrintDirectiveDefinitions()) + ->setPrintDirectivesInDescription($settings->isPrintDirectivesInDescription()) + ->setPrintUnusedDefinitions($settings->isPrintUnusedDefinitions()) + ->setNormalizeSchema($settings->isNormalizeSchema()) + ->setNormalizeUnions($settings->isNormalizeUnions()) + ->setNormalizeEnums($settings->isNormalizeEnums()) + ->setNormalizeInterfaces($settings->isNormalizeInterfaces()) + ->setNormalizeFields($settings->isNormalizeFields()) + ->setNormalizeArguments($settings->isNormalizeArguments()) + ->setNormalizeDescription($settings->isNormalizeDescription()) + ->setNormalizeDirectiveLocations($settings->isNormalizeDirectiveLocations()) + ->setAlwaysMultilineUnions($settings->isAlwaysMultilineUnions()) + ->setAlwaysMultilineInterfaces($settings->isAlwaysMultilineInterfaces()) + ->setAlwaysMultilineDirectiveLocations($settings->isAlwaysMultilineDirectiveLocations()) + ->setDirectiveFilter($settings->getDirectiveFilter()); + } +} diff --git a/src/SchemaPrinter/Settings/ImmutableSettingsTest.php b/src/SchemaPrinter/Settings/ImmutableSettingsTest.php new file mode 100644 index 00000000..8e1b27f2 --- /dev/null +++ b/src/SchemaPrinter/Settings/ImmutableSettingsTest.php @@ -0,0 +1,31 @@ +getMethods(ReflectionMethod::IS_PUBLIC); + $settings = Mockery::mock(Settings::class); + + foreach ($methods as $method) { + $settings + ->shouldReceive($method->getName()) + ->once(); + } + + ImmutableSettings::createFrom($settings); + } +} diff --git a/src/SearchBy/Ast/UsageTest.php b/src/SearchBy/Ast/UsageTest.php index 07ca6485..8dcdcd20 100644 --- a/src/SearchBy/Ast/UsageTest.php +++ b/src/SearchBy/Ast/UsageTest.php @@ -2,11 +2,10 @@ namespace LastDragon_ru\LaraASP\GraphQL\SearchBy\Ast; +use LastDragon_ru\LaraASP\GraphQL\Testing\Package\TestCase; use LogicException; use OutOfBoundsException; -use PHPUnit\Framework\TestCase; use stdClass; - use function sprintf; /** diff --git a/src/Testing/Package/SchemaPrinter/TestSettings.php b/src/Testing/Package/SchemaPrinter/TestSettings.php new file mode 100644 index 00000000..3bbeb235 --- /dev/null +++ b/src/Testing/Package/SchemaPrinter/TestSettings.php @@ -0,0 +1,54 @@ +filter)($directive); + } + }; + } + + return parent::setDirectiveFilter($value); + } +}