Skip to content

Commit

Permalink
GraphQL Schema Diff Command (#18)
Browse files Browse the repository at this point in the history
* Add graphql schema diff to feature branch.

* Add back services printer.

* Total rewrite started.

* Someday I will stop re-writing this feature...

But it is not today.

* More things.

* This almost works now.

* Splitting out and making non-interactive work.

* Fix schema comparator.

* Add schema diff command.

* Implement interfaces.

* Remove unused use.

* Allow extends as interfaces and map inputs correctly.

* Fixing updates.

* Fix issues with diffing fields and args.

* Removing field improvements.

* Fix issue with updating types.

* Fix input type diff detection.

* Update apple.

* Remove the services printer.

* Remove unused function.

* Adding argument and value diff tests.

* Add tests for enum and schema.

* More comparator tests.

* Testing the new command.

* Remove unused tests for now.

* Adding tests for graphql code helper.

* Additional tests.
  • Loading branch information
midnightLuke committed May 22, 2024
1 parent 6507b1b commit 2f7c32b
Show file tree
Hide file tree
Showing 40 changed files with 4,238 additions and 5 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"psr/http-message": "^1.1",
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"psr/log": "^3.0"
"psr/log": "^3.0",
"sebastian/diff": "^5.1"
},
"suggest": {
"league/event": "This package ships configuration that works with this package.",
Expand Down
903 changes: 903 additions & 0 deletions src/Command/GraphQLGenerateFromSchema.php

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions src/Command/GraphQLSchemaDiffCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace ForestCityLabs\Framework\Command;

use GraphQL\Language\Parser;
use GraphQL\Type\Schema;
use GraphQL\Utils\BuildSchema;
use GraphQL\Utils\SchemaPrinter;
use SebastianBergmann\Diff\Differ;
use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class GraphQLSchemaDiffCommand extends Command
{
public function __construct(
private string $schema_file,
private Schema $schema,
) {
parent::__construct('graphql:schema-diff');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
// The desired schema is loaded from a graphql file.
$new = BuildSchema::build(
Parser::parse(file_get_contents($this->schema_file))
);

// The current schema is found in code.
$old = $this->schema;

$differ = new Differ(new UnifiedDiffOutputBuilder());
$opts = ['sortArguments' => true, 'sortEnumValues' => true, 'sortFields' => true, 'sortInputFields' => true, 'sortTypes' => true];
$diff = $differ->diff(
SchemaPrinter::doPrint($old, $opts),
SchemaPrinter::doPrint($new, $opts),
);

foreach (preg_split("/\r\n|\n|\r/", $diff) as $line) {
if (substr($line, 0, 1) === '-') {
$output->writeln('<fg=red>' . $line . '</>');
} elseif (substr($line, 0, 1) === '+') {
$output->writeln('<fg=green>' . $line . '</>');
} else {
$output->writeln($line);
}
}

return Command::SUCCESS;
}
}
91 changes: 91 additions & 0 deletions src/GraphQL/Diff/ArgumentDiff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace ForestCityLabs\Framework\GraphQL\Diff;

use GraphQL\Type\Definition\Argument;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType;

class ArgumentDiff
{
public function __construct(
private readonly Argument $old_argument,
private readonly Argument $new_argument
) {
}

public function getOldArgument(): Argument
{
return $this->old_argument;
}

public function getNewArgument(): Argument
{
return $this->new_argument;
}

public function isNameDifferent(): bool
{
return $this->old_argument->name !== $this->new_argument->name;
}

public function isDescriptionDifferent(): bool
{
return $this->old_argument->description !== $this->new_argument->description;
}
public function isTypeDifferent(): bool
{
$old_type = Type::getNamedType($this->old_argument->getType());
$new_type = Type::getNamedType($this->new_argument->getType());
return $old_type->name !== $new_type->name;
}

public function isListDifferent(): bool
{
$old_list = false;
$new_list = false;
$old_type = $this->old_argument->getType();
$new_type = $this->new_argument->getType();
while ($old_type instanceof WrappingType) {
if ($old_type instanceof ListOfType) {
$old_list = true;
}
$old_type = $old_type->getWrappedType();
}
while ($new_type instanceof WrappingType) {
if ($new_type instanceof ListOfType) {
$new_list = true;
}
$new_type = $new_type->getWrappedType();
}
return $new_list !== $old_list;
}

public function isNonNullDifferent(): bool
{
$old_non_null = false;
$new_non_null = false;
$old_type = $this->old_argument->getType();
$new_type = $this->new_argument->getType();
if ($old_type instanceof NonNull) {
$old_non_null = true;
}
if ($new_type instanceof NonNull) {
$new_non_null = true;
}
return $new_non_null !== $old_non_null;
}

public function isDifferent(): bool
{
return ($this->isNameDifferent()
|| $this->isDescriptionDifferent()
|| $this->isTypeDifferent()
|| $this->isListDifferent()
|| $this->isNonNullDifferent());
}
}
64 changes: 64 additions & 0 deletions src/GraphQL/Diff/EnumTypeDiff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace ForestCityLabs\Framework\GraphQL\Diff;

use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\NamedType;

class EnumTypeDiff implements TypeDiff
{
public function __construct(
private readonly EnumType $old_enum,
private readonly EnumType $new_enum,
private readonly array $new_values,
private readonly array $altered_values,
private readonly array $dropped_values
) {
}

public function isNameDifferent(): bool
{
return $this->old_enum->name() !== $this->new_enum->name();
}

public function isDescriptionDifferent(): bool
{
return $this->old_enum->description() !== $this->new_enum->description();
}

public function getOldType(): NamedType
{
return $this->old_enum;
}

public function getNewType(): NamedType
{
return $this->new_enum;
}

public function getNewValues(): array
{
return $this->new_values;
}

public function getAlteredValues(): array
{
return $this->altered_values;
}

public function getDroppedValues(): array
{
return $this->dropped_values;
}

public function isDifferent(): bool
{
return ($this->isNameDifferent()
|| $this->isDescriptionDifferent()
|| count($this->new_values) > 0
|| count($this->altered_values) > 0
|| count($this->dropped_values) > 0);
}
}
119 changes: 119 additions & 0 deletions src/GraphQL/Diff/FieldDiff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

declare(strict_types=1);

namespace ForestCityLabs\Framework\GraphQL\Diff;

use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType;

class FieldDiff
{
public function __construct(
private readonly FieldDefinition $old_field,
private readonly FieldDefinition $new_field,
private readonly array $new_arguments,
private readonly array $altered_arguments,
private readonly array $dropped_arguments
) {
}

public function getOldField(): FieldDefinition
{
return $this->old_field;
}

public function getNewField(): FieldDefinition
{
return $this->new_field;
}

public function getNewArguments(): array
{
return $this->new_arguments;
}

public function getAlteredArguments(): array
{
return $this->altered_arguments;
}

public function getDroppedArguments(): array
{
return $this->dropped_arguments;
}

public function isNameDifferent(): bool
{
return $this->old_field->getName() !== $this->new_field->getName();
}

public function isDescriptionDifferent(): bool
{
return $this->old_field->description !== $this->new_field->description;
}

public function isTypeDifferent(): bool
{
$old_type = Type::getNamedType($this->old_field->getType());
$new_type = Type::getNamedType($this->new_field->getType());
return $old_type->name !== $new_type->name;
}

public function isListDifferent(): bool
{
$old_list = false;
$new_list = false;
$old_type = $this->old_field->getType();
$new_type = $this->new_field->getType();
while ($old_type instanceof WrappingType) {
if ($old_type instanceof ListOfType) {
$old_list = true;
}
$old_type = $old_type->getWrappedType();
}
while ($new_type instanceof WrappingType) {
if ($new_type instanceof ListOfType) {
$new_list = true;
}
$new_type = $new_type->getWrappedType();
}
return $new_list !== $old_list;
}

public function isNonNullDifferent(): bool
{
$old_non_null = false;
$new_non_null = false;
$old_type = $this->old_field->getType();
$new_type = $this->new_field->getType();
if ($old_type instanceof NonNull) {
$old_non_null = true;
}
if ($new_type instanceof NonNull) {
$new_non_null = true;
}
return $new_non_null !== $old_non_null;
}

public function isDeprecationReasonDifferent(): bool
{
return $this->old_field->deprecationReason !== $this->new_field->deprecationReason;
}

public function isDifferent(): bool
{
return ($this->isNameDifferent()
|| $this->isDescriptionDifferent()
|| $this->isDeprecationReasonDifferent()
|| $this->isTypeDifferent()
|| $this->isListDifferent()
|| $this->isNonNullDifferent()
|| count($this->new_arguments) > 0
|| count($this->altered_arguments) > 0
|| count($this->dropped_arguments) > 0);
}
}
Loading

0 comments on commit 2f7c32b

Please sign in to comment.