Skip to content

Commit

Permalink
add lock manager to detect api changes
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskapp committed Feb 21, 2025
1 parent 8991d95 commit d6d9ec3
Show file tree
Hide file tree
Showing 8 changed files with 477 additions and 7 deletions.
93 changes: 93 additions & 0 deletions src/Console/LockCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
/*
* PSX is an open source PHP framework to develop RESTful APIs.
* For the current version and information visit <https://phpsx.org>
*
* Copyright (c) Christoph Kappestein <christoph.kappestein@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace PSX\Api\Console;

use PSX\Api\Exception\BreakingChangesException;
use PSX\Api\Exception\LockException;
use PSX\Api\LockManager;
use PSX\Api\Scanner\FilterFactoryInterface;
use PSX\Api\ScannerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
* LockCommand
*
* @author Christoph Kappestein <christoph.kappestein@gmail.com>
* @license http://www.apache.org/licenses/LICENSE-2.0
* @link https://phpsx.org
*/
class LockCommand extends Command
{
public function __construct(private LockManager $lockManager, private ScannerInterface $scanner, private FilterFactoryInterface $filterFactory)
{
parent::__construct();
}

protected function configure(): void
{
$this
->setName('api:lock')
->setDescription('Generates or verifies an API lock file, this protects')
->addArgument('goal', InputArgument::REQUIRED, 'Either "generate" or "verify"')
->addOption('file', 'f', InputOption::VALUE_REQUIRED, 'Optional a specific target lock file');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$filterName = $input->getOption('filter');
if (empty($filterName)) {
$filterName = $this->filterFactory->getDefault();
}

$file = $input->getOption('file');
if (empty($file)) {
$file = getcwd() . '/api.lock';
}

$filter = $this->filterFactory->getFilter($filterName);
$spec = $this->scanner->generate($filter);

try {
$goal = $input->getArgument('goal');
if ($goal === 'generate') {
$this->lockManager->lock($spec, $file);
} elseif ($goal === 'verify') {
$this->lockManager->verify($spec, $file);
} else {
throw new LockException('Provided an invalid lock goal, must be either "generate" or "verify"');
}
} catch (BreakingChangesException $e) {
$output->writeln('Error: ' . $e->getMessage());

return self::FAILURE;
} catch (LockException $e) {
$output->writeln('Error: ' . $e->getMessage());

return self::FAILURE;
}

return self::SUCCESS;
}
}
2 changes: 1 addition & 1 deletion src/Console/ParseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$content = $generator->generate($specification);

if ($content instanceof Chunks) {
$content->writeTo($dir . '/sdk-' . $format . '.zip');
$content->writeToZip($dir . '/sdk-' . $format . '.zip');
} else {
file_put_contents($dir . '/output-' . $format . '.' . $extension, $content);
}
Expand Down
41 changes: 41 additions & 0 deletions src/Exception/BreakingChangesException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/*
* PSX is an open source PHP framework to develop RESTful APIs.
* For the current version and information visit <https://phpsx.org>
*
* Copyright (c) Christoph Kappestein <christoph.kappestein@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace PSX\Api\Exception;

/**
* BreakingChangesException
*
* @author Christoph Kappestein <christoph.kappestein@gmail.com>
* @license http://www.apache.org/licenses/LICENSE-2.0
* @link https://phpsx.org
*/
class BreakingChangesException extends LockException
{
public function __construct(private array $changes)
{
parent::__construct();
}

public function getChanges(): array
{
return $this->changes;
}
}
32 changes: 32 additions & 0 deletions src/Exception/LockException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
/*
* PSX is an open source PHP framework to develop RESTful APIs.
* For the current version and information visit <https://phpsx.org>
*
* Copyright (c) Christoph Kappestein <christoph.kappestein@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace PSX\Api\Exception;

/**
* LockException
*
* @author Christoph Kappestein <christoph.kappestein@gmail.com>
* @license http://www.apache.org/licenses/LICENSE-2.0
* @link https://phpsx.org
*/
class LockException extends ApiException
{
}
6 changes: 0 additions & 6 deletions src/Inspector/ChangelogGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ public function __construct()
$this->changelogGenerator = new SchemaChangelogGenerator();
}

/**
* @throws OperationNotFoundException
*/
public function generate(SpecificationInterface $left, SpecificationInterface $right): \Generator
{
if ($left->getBaseUrl() !== $right->getBaseUrl()) {
Expand Down Expand Up @@ -90,9 +87,6 @@ private function generateSecurity(SecurityInterface $left, SecurityInterface $ri
}
}

/**
* @throws OperationNotFoundException
*/
private function generateCollection(OperationsInterface $left, OperationsInterface $right): \Generator
{
foreach ($left->getAll() as $name => $operation) {
Expand Down
90 changes: 90 additions & 0 deletions src/LockManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php
/*
* PSX is an open source PHP framework to develop RESTful APIs.
* For the current version and information visit <https://phpsx.org>
*
* Copyright (c) Christoph Kappestein <christoph.kappestein@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace PSX\Api;

use PSX\Api\Exception\BreakingChangesException;
use PSX\Api\Exception\GeneratorException;
use PSX\Api\Exception\LockException;
use PSX\Api\Exception\ParserException;
use PSX\Api\Inspector\ChangelogGenerator;
use PSX\Schema\Inspector\SemVer;
use PSX\Schema\SchemaManagerInterface;

/**
* The lock manager can help to ensure that your API does not introduce any breaking changes within a minor version.
* Therefor you need to generate a lock file at the start of a major version, then you can verify every change
* of the specification against this lock file and if a breaking change is introduced the verify method throws an exception
*
* @author Christoph Kappestein <christoph.kappestein@gmail.com>
* @license http://www.apache.org/licenses/LICENSE-2.0
* @link https://phpsx.org
*/
class LockManager
{
private ChangelogGenerator $changelogGenerator;

public function __construct(private readonly SchemaManagerInterface $schemaManager)
{
$this->changelogGenerator = new ChangelogGenerator();
}

/**
* @throws LockException
*/
public function lock(SpecificationInterface $specification, string $lockFile): void
{
try {
file_put_contents($lockFile, (string) (new Generator\Spec\TypeAPI())->generate($specification));
} catch (GeneratorException $e) {
throw new LockException('Could not generate lock file: ' . $e->getMessage(), previous: $e);
}
}

/**
* @throws LockException
* @throws BreakingChangesException
*/
public function verify(SpecificationInterface $specification, string $lockFile): void
{
if (!is_file($lockFile)) {
throw new LockException('Provided lock file does not exist: ' . $lockFile);
}

try {
$lockSpecification = (new Parser\TypeAPI($this->schemaManager))->parse(file_get_contents($lockFile));
} catch (ParserException $e) {
throw new LockException('Could not parse provided lock file ' . $lockFile . ' got: ' . $e->getMessage(), previous: $e);
}

$changelogs = $this->changelogGenerator->generate($lockSpecification, $specification);

$breakingChanges = [];
foreach ($changelogs as $level => $message) {
if ($level === SemVer::MAJOR) {
$breakingChanges[] = $message;
}
}

if (count($breakingChanges) > 0) {
throw new BreakingChangesException($breakingChanges);
}
}
}
81 changes: 81 additions & 0 deletions tests/LockManagerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php
/*
* PSX is an open source PHP framework to develop RESTful APIs.
* For the current version and information visit <https://phpsx.org>
*
* Copyright (c) Christoph Kappestein <christoph.kappestein@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace PSX\Api\Tests;

use PHPUnit\Framework\TestCase;
use PSX\Api\Exception\BreakingChangesException;
use PSX\Api\LockManager;
use PSX\Api\Operation;
use PSX\Api\Operations;
use PSX\Api\SpecificationInterface;
use PSX\Schema\SchemaManager;
use PSX\Schema\TypeFactory;

/**
* LockManagerTest
*
* @author Christoph Kappestein <christoph.kappestein@gmail.com>
* @license http://www.apache.org/licenses/LICENSE-2.0
* @link https://phpsx.org
*/
class LockManagerTest extends ApiManagerTestCase
{
public function testVerifyBreakingChanges(): void
{
$lockManager = new LockManager(new SchemaManager());

$specification = $this->apiManager->getApi(__DIR__ . '/Parser/typeapi/simple.json');

$lockFile = __DIR__ . '/api.lock';
$lockManager->lock($specification, $lockFile);

try {
$specification = $this->apiManager->getApi(__DIR__ . '/Parser/typeapi/simple_bc.json');

$lockManager->verify($specification, $lockFile);

$this->fail('Must throw a breaking change exception');
} catch (BreakingChangesException $e) {
$this->assertSame(3, count($e->getChanges()));
$this->assertSame([
'Operation "test.get.arguments.integer" was removed',
'Property "Rating.text" was removed',
'Property "Song.length" was removed',
], $e->getChanges());
}
}

public function testVerifyNoBreakingChanges(): void
{
$lockManager = new LockManager(new SchemaManager());

$specification = $this->apiManager->getApi(__DIR__ . '/Parser/typeapi/simple.json');

$lockFile = __DIR__ . '/api.lock';
$lockManager->lock($specification, $lockFile);

$specification = $this->apiManager->getApi(__DIR__ . '/Parser/typeapi/simple.json');

$lockManager->verify($specification, $lockFile);

$this->assertInstanceOf(SpecificationInterface::class, $specification);
}
}
Loading

0 comments on commit d6d9ec3

Please sign in to comment.