Skip to content

Commit

Permalink
apply abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
Ferror committed Dec 27, 2023
1 parent ada56c7 commit 7033eff
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 17 deletions.
32 changes: 32 additions & 0 deletions src/Collection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Ferror\OpenapiCoverage;

final readonly class Collection
{
public static function create(array $items): self
{
return new self($items);
}

public function __construct(public array $items)
{
}

public function filter(callable $callable): self
{
return new self(array_filter($this->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)));
}
}
17 changes: 17 additions & 0 deletions src/Coverage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Ferror\OpenapiCoverage;

final readonly class Coverage
{
public function __construct(public float $value)
{
}

public function asPercentage(): float
{
return round($this->value * 100, 2);
}
}
28 changes: 28 additions & 0 deletions src/CoverageCalculator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Ferror\OpenapiCoverage;

use InvalidArgumentException;

final readonly class CoverageCalculator
{
public function __construct(
private int $numberOfPaths,
private int $numberOfDocumentedPaths,
) {
if ($numberOfPaths < 0 || $numberOfDocumentedPaths < 0) {
throw new InvalidArgumentException();
}
}

public function calculate(): Coverage
{
if ($this->numberOfPaths <= 0) {
return new Coverage(0.0);
}

return new Coverage($this->numberOfDocumentedPaths / $this->numberOfPaths);
}
}
19 changes: 19 additions & 0 deletions src/Route.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Ferror\OpenapiCoverage;

final readonly class Route
{
public function __construct(
public string $path,
public string $method,
) {
}

public function equals(self $self): bool
{
return $this->path === $self->path && $this->method && $self->method;
}
}
41 changes: 27 additions & 14 deletions src/Symfony/Console/CheckCoverageCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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!');
Expand Down
7 changes: 7 additions & 0 deletions tests/Integration/CheckCoverageCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,12 @@ public function testExecuteClass(): void
$commandTester->assertCommandIsSuccessful();

$display = $commandTester->getDisplay();

$expectedDisplay = <<<TEXT
Open API coverage: 0%
OpenAPI schema covers all Symfony routes. Good job!
TEXT;

$this->assertEquals($expectedDisplay, $display);
}
}
6 changes: 3 additions & 3 deletions tests/Integration/Service/config/routes.yaml
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions tests/Unit/CollectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Ferror\OpenapiCoverage\Unit;

use Ferror\OpenapiCoverage\Collection;
use PHPUnit\Framework\TestCase;

final class CollectionTest extends TestCase
{
public function testFilter(): void
{
$collection = new Collection(['item', 'not-item']);

$collection = $collection->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);
}
}
34 changes: 34 additions & 0 deletions tests/Unit/CoverageCalculatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Ferror\OpenapiCoverage\Unit;

use Ferror\OpenapiCoverage\Coverage;
use Ferror\OpenapiCoverage\CoverageCalculator;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;

final class CoverageCalculatorTest extends TestCase
{
public function testCalculate(): void
{
$calculator = new CoverageCalculator(1, 1);

$this->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);
}
}

0 comments on commit 7033eff

Please sign in to comment.