From 7033eff437b7c70a6db0fecb73e2e8e5c593028e Mon Sep 17 00:00:00 2001 From: Ferror Date: Wed, 27 Dec 2023 02:01:31 +0100 Subject: [PATCH] apply abstraction --- src/Collection.php | 32 +++++++++++++++ src/Coverage.php | 17 ++++++++ src/CoverageCalculator.php | 28 +++++++++++++ src/Route.php | 19 +++++++++ src/Symfony/Console/CheckCoverageCommand.php | 41 ++++++++++++------- .../Integration/CheckCoverageCommandTest.php | 7 ++++ tests/Integration/Service/config/routes.yaml | 6 +-- tests/Unit/CollectionTest.php | 38 +++++++++++++++++ tests/Unit/CoverageCalculatorTest.php | 34 +++++++++++++++ 9 files changed, 205 insertions(+), 17 deletions(-) create mode 100644 src/Collection.php create mode 100644 src/Coverage.php create mode 100644 src/CoverageCalculator.php create mode 100644 src/Route.php create mode 100644 tests/Unit/CollectionTest.php create mode 100644 tests/Unit/CoverageCalculatorTest.php diff --git a/src/Collection.php b/src/Collection.php new file mode 100644 index 0000000..8ae0f8d --- /dev/null +++ b/src/Collection.php @@ -0,0 +1,32 @@ +items, $callable)); + } + + public function map(callable $callable): self + { + return new self(array_map($callable, $this->items)); + } + + public function diff(array $items): self + { + return new self(array_values(array_diff($this->items, $items))); + } +} diff --git a/src/Coverage.php b/src/Coverage.php new file mode 100644 index 0000000..1c4741a --- /dev/null +++ b/src/Coverage.php @@ -0,0 +1,17 @@ +value * 100, 2); + } +} diff --git a/src/CoverageCalculator.php b/src/CoverageCalculator.php new file mode 100644 index 0000000..211d96d --- /dev/null +++ b/src/CoverageCalculator.php @@ -0,0 +1,28 @@ +numberOfPaths <= 0) { + return new Coverage(0.0); + } + + return new Coverage($this->numberOfDocumentedPaths / $this->numberOfPaths); + } +} diff --git a/src/Route.php b/src/Route.php new file mode 100644 index 0000000..662123e --- /dev/null +++ b/src/Route.php @@ -0,0 +1,19 @@ +path === $self->path && $this->method && $self->method; + } +} diff --git a/src/Symfony/Console/CheckCoverageCommand.php b/src/Symfony/Console/CheckCoverageCommand.php index 0c48d98..6f2a601 100644 --- a/src/Symfony/Console/CheckCoverageCommand.php +++ b/src/Symfony/Console/CheckCoverageCommand.php @@ -4,43 +4,56 @@ namespace Ferror\OpenapiCoverage\Symfony\Console; +use Ferror\OpenapiCoverage\Collection; +use Ferror\OpenapiCoverage\CoverageCalculator; +use Ferror\OpenapiCoverage\Route; use Nelmio\ApiDocBundle\Render\RenderOpenApi; +use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouterInterface; class CheckCoverageCommand extends Command { public function __construct( - private RouterInterface $router, - private RenderOpenApi $renderOpenApi, - private array $excludedPaths = [], + private readonly RouterInterface $router, + private readonly RenderOpenApi $renderOpenApi, + private readonly ?LoggerInterface $logger = null, + private readonly array $excludedPaths = [], + private readonly string $prefix = '/v1', ) { parent::__construct('ferror:check-openapi-coverage'); } public function execute(InputInterface $input, OutputInterface $output): int { - $symfonyPaths = $this->router->getRouteCollection(); + $paths = []; - $symfonyPaths = array_map(fn (Route $route) => $route->getPath(), $symfonyPaths->all()); - $symfonyPaths = array_values($symfonyPaths); - $symfonyPaths = array_filter($symfonyPaths, fn (string $route) => str_starts_with($route, '/risk/v1')); - $symfonyPaths = array_map(fn (string $route) => str_replace('/risk/v1', '', $route), $symfonyPaths); - $symfonyPaths = array_filter($symfonyPaths, fn(string $route) => !in_array($route, $this->excludedPaths, true)); + foreach ($this->router->getRouteCollection()->getIterator() as $route) { + foreach ($route->getMethods() as $method) { + $paths[] = new Route($route->getPath(), $method); + } + } + + $paths = Collection::create($paths) + ->filter(fn (Route $route) => str_starts_with($route->path, $this->prefix)) + ->map(fn (Route $route) => str_replace($this->prefix, '', $route->path)) + ->filter(fn(Route $route) => !in_array($route->path, $this->excludedPaths, true)) + ; $openApi = $this->renderOpenApi->render('json', 'default'); $openApi = json_decode($openApi, true, 512, JSON_THROW_ON_ERROR); $openApiPaths = array_keys($openApi['paths']); - $missingPaths = array_diff($symfonyPaths, $openApiPaths); + $this->logger?->debug('CoverageCommand: Open Api Paths ', ['open_api_paths_count' => count($openApiPaths)]); + + $missingPaths = array_diff($paths->items, $openApiPaths); + + $coverageCalculator = new CoverageCalculator(count($paths->items), count($openApiPaths)); - $missingPathCoverage = count($openApiPaths) / (count($symfonyPaths) <= 0 ? 1 : count($symfonyPaths)); - $output->writeln('Open API coverage: ' . round($missingPathCoverage * 100, 2) . '%'); + $output->writeln('Open API coverage: ' . $coverageCalculator->calculate()->asPercentage() . '%'); if (empty($missingPaths)) { $output->writeln('OpenAPI schema covers all Symfony routes. Good job!'); diff --git a/tests/Integration/CheckCoverageCommandTest.php b/tests/Integration/CheckCoverageCommandTest.php index e3a47e3..f42c488 100644 --- a/tests/Integration/CheckCoverageCommandTest.php +++ b/tests/Integration/CheckCoverageCommandTest.php @@ -22,5 +22,12 @@ public function testExecuteClass(): void $commandTester->assertCommandIsSuccessful(); $display = $commandTester->getDisplay(); + + $expectedDisplay = <<assertEquals($expectedDisplay, $display); } } diff --git a/tests/Integration/Service/config/routes.yaml b/tests/Integration/Service/config/routes.yaml index e10b465..db12654 100644 --- a/tests/Integration/Service/config/routes.yaml +++ b/tests/Integration/Service/config/routes.yaml @@ -1,11 +1,11 @@ rest_product_get: - path: /products + path: /v1/products methods: GET rest_product_post: - path: /products + path: /v1/products methods: POST rest_product_delete: - path: /products/:id + path: /v1/products/:id methods: DELETE diff --git a/tests/Unit/CollectionTest.php b/tests/Unit/CollectionTest.php new file mode 100644 index 0000000..814f25e --- /dev/null +++ b/tests/Unit/CollectionTest.php @@ -0,0 +1,38 @@ +filter(fn (string $item) => $item === 'item'); + + $this->assertEquals(['item'], $collection->items); + } + + public function testDiff(): void + { + $collection = new Collection(['item-1', 'item-2']); + + $collection = $collection->diff(['item-1']); + + $this->assertEquals(['item-2'], $collection->items); + } + + public function testMap(): void + { + $collection = new Collection(['item', 'item']); + + $collection = $collection->map(fn (string $item) => $item . '-not'); + + $this->assertEquals(['item-not', 'item-not'], $collection->items); + } +} diff --git a/tests/Unit/CoverageCalculatorTest.php b/tests/Unit/CoverageCalculatorTest.php new file mode 100644 index 0000000..f86d7fa --- /dev/null +++ b/tests/Unit/CoverageCalculatorTest.php @@ -0,0 +1,34 @@ +assertEquals(new Coverage(1), $calculator->calculate()); + } + + public function testThrowsExceptionOnNegativePaths(): void + { + $this->expectException(InvalidArgumentException::class); + + new CoverageCalculator(-1, 1); + } + + public function testThrowsExceptionOnNegativeDocumentedPaths(): void + { + $this->expectException(InvalidArgumentException::class); + + new CoverageCalculator(1, -1); + } +}