Skip to content

Commit

Permalink
feat(graphql): Awesome SchemaPrinter with directives, filtering, an…
Browse files Browse the repository at this point in the history
…d advanced formating.

These are few known issues:
* Schema description is not yet supported by `graphql-php` (webonyx/graphql-php#1027)
* "implements" for interfaces is not yet supported by Lighthouse (just for the reference webonyx/graphql-php#728)
  • Loading branch information
LastDragon-ru committed Jan 30, 2022
2 parents 1b9977a + 70b8d3f commit 8f85614
Show file tree
Hide file tree
Showing 90 changed files with 11,110 additions and 2 deletions.
53 changes: 53 additions & 0 deletions src/SchemaPrinter/Blocks/Ast/ArgumentNodeList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Ast;

use GraphQL\Language\AST\ArgumentNode;
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\BlockList;
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Property;
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Misc\PrinterSettings;
use Traversable;

/**
* @internal
* @extends BlockList<Property<ValueNodeBlock>>
*/
class ArgumentNodeList extends BlockList {
/**
* @param Traversable<ArgumentNode>|array<ArgumentNode> $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();
}
}
53 changes: 53 additions & 0 deletions src/SchemaPrinter/Blocks/Ast/DirectiveNodeBlock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Ast;

use GraphQL\Language\AST\DirectiveNode;
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Block;
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Named;
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Misc\PrinterSettings;

use function mb_strlen;

/**
* @internal
*/
class DirectiveNodeBlock extends Block implements Named {
public function __construct(
PrinterSettings $settings,
int $level,
int $used,
private DirectiveNode $node,
) {
parent::__construct($settings, $level, $used);
}

public function getName(): string {
return "@{$this->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}";
}
}
127 changes: 127 additions & 0 deletions src/SchemaPrinter/Blocks/Ast/DirectiveNodeBlockTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Ast;

use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\Parser;
use GraphQL\Language\Printer;
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Contracts\Settings;
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Misc\DirectiveResolver;
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Misc\PrinterSettings;
use LastDragon_ru\LaraASP\GraphQL\Testing\Package\SchemaPrinter\TestSettings;
use LastDragon_ru\LaraASP\GraphQL\Testing\Package\TestCase;

/**
* @internal
* @coversDefaultClass \LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Ast\DirectiveNodeBlock
*/
class DirectiveNodeBlockTest extends TestCase {
// <editor-fold desc="Tests">
// =========================================================================
/**
* @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());
}
// </editor-fold>

// <editor-fold desc="DataProviders">
// =========================================================================
/**
* @return array<string,array{string, Settings, int, int, DirectiveNode}>
*/
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")'),
],
];
}
// </editor-fold>
}
81 changes: 81 additions & 0 deletions src/SchemaPrinter/Blocks/Ast/DirectiveNodeList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Ast;

use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\Parser;
use GraphQL\Type\Definition\Directive;
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\Block;
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Blocks\BlockList;
use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Misc\PrinterSettings;
use Traversable;

use function json_encode;

/**
* @internal
* @extends BlockList<DirectiveNodeBlock>
*/
class DirectiveNodeList extends BlockList {
/**
* @param Traversable<DirectiveNode>|array<DirectiveNode> $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,
);
}
}
Loading

0 comments on commit 8f85614

Please sign in to comment.