Skip to content

Commit

Permalink
Merge pull request #20 from membrane-php/infection
Browse files Browse the repository at this point in the history
Add Infection
  • Loading branch information
carnage authored Sep 25, 2023
2 parents d4066b7 + bb843ea commit 698a6aa
Show file tree
Hide file tree
Showing 17 changed files with 696 additions and 152 deletions.
8 changes: 7 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"symfony/console": "^6.2"
},
"require-dev": {
"infection/infection": "^0.27.0",
"phpunit/phpunit": "^10.2",
"phpstan/phpstan": "^1.8",
"squizlabs/php_codesniffer": "^3.7",
Expand All @@ -26,5 +27,10 @@
},
"bin": [
"bin/membrane-router"
]
],
"config": {
"allow-plugins": {
"infection/extension-installer": true
}
}
}
11 changes: 11 additions & 0 deletions infection.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "vendor/infection/infection/resources/schema.json",
"source": {
"directories": [
"src"
]
},
"mutators": {
"@default": true
}
}
17 changes: 8 additions & 9 deletions src/Exception/CannotRouteRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,20 @@

/* This exception occurs when the request does not match any routes in your route collection. */

class CannotRouteRequest extends \RuntimeException
use RuntimeException;

class CannotRouteRequest extends RuntimeException
{
public const NOT_FOUND = 404;
public const METHOD_NOT_ALLOWED = 405;

public static function fromErrorCode(int $errorCode): self
{
switch ($errorCode) {
case self::NOT_FOUND:
return self::notFound();
case self::METHOD_NOT_ALLOWED:
return self::methodNotAllowed();
default:
return new self();
}
assert($errorCode === 404 || $errorCode === 405);
return match ($errorCode) {
self::NOT_FOUND => self::notFound(),
self::METHOD_NOT_ALLOWED => self::methodNotAllowed(),
};
}

public static function notFound(): self
Expand Down
15 changes: 10 additions & 5 deletions src/Route/Path.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@

final class Path implements JsonSerializable
{
public readonly string $regex;
/** @var array<string, string> */
private array $operations = [];

public function __construct(
public readonly string $url,
public readonly string $regex,
public readonly string $url
) {
$regex = preg_replace('#{[^/]+}#', '([^/]+)', $this->url);
assert(is_string($regex));
$this->regex = $regex;
}

public function addRoute(Route $route): void
public function addRoute(string $method, string $operationId): void
{
$this->operations[$route->method] = $route->operationId;
$this->operations[$method] = $operationId;
}

public function isDynamic(): bool
Expand All @@ -40,6 +43,8 @@ public function isEmpty(): bool
/** @return array<string, string> */
public function jsonSerialize(): array
{
return [...$this->operations];
$operations = $this->operations;
ksort($operations);
return $operations;
}
}
16 changes: 0 additions & 16 deletions src/Route/Route.php

This file was deleted.

31 changes: 14 additions & 17 deletions src/Route/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,26 @@

final class Server implements JsonSerializable
{
public readonly string $regex;

/** @var array<string, Path>*/
private array $paths = [];

public function __construct(
public readonly string $url,
public readonly string $regex,
public readonly string $url
) {
$regex = preg_replace('#{[^/]+}#', '([^/]+)', $this->url);
assert(is_string($regex));
$this->regex = $regex;
}

public function addRoute(Route $route): void
public function addRoute(string $pathUrl, string $method, string $operationId): void
{
if (!isset($this->paths[$route->path])) {
$this->addPath(new Path($route->path, $route->pathRegex));
if (!isset($this->paths[$pathUrl])) {
$this->paths[$pathUrl] = new Path($pathUrl);
}

$this->paths[$route->path]->addRoute($route);
$this->paths[$pathUrl]->addRoute($method, $operationId);
}

public function isDynamic(): bool
Expand All @@ -38,7 +42,7 @@ public function howManyDynamicComponents(): int

public function isEmpty(): bool
{
return count(array_filter($this->paths, fn($p) => !$p->isEmpty())) === 0;
return count($this->paths) === 0;
}

public function isHosted(): bool
Expand All @@ -53,14 +57,14 @@ public function isHosted(): bool
*/
public function jsonSerialize(): array
{
$filteredPaths = array_filter($this->paths, fn($p) => !$p->isEmpty());
$paths = $this->paths;
usort(
$filteredPaths,
$paths,
fn(Path $a, Path $b) => $a->howManyDynamicComponents() <=> $b->howManyDynamicComponents()
);

$staticPaths = $dynamicPaths = $regex = [];
foreach ($filteredPaths as $path) {
foreach ($paths as $path) {
if ($path->isDynamic()) {
$dynamicPaths[$path->url] = $path->jsonSerialize();
$regex[] = sprintf('%s(*MARK:%s)', $path->regex, $path->url);
Expand All @@ -77,11 +81,4 @@ public function jsonSerialize(): array
]
];
}

private function addPath(Path $path): void
{
if (!isset($this->paths[$path->url])) {
$this->paths[$path->url] = $path;
}
}
}
8 changes: 4 additions & 4 deletions src/RouteCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Membrane\OpenAPIRouter\Route\Server;

class RouteCollection
final class RouteCollection
{
/**
* @param array{
Expand Down Expand Up @@ -45,14 +45,14 @@ public function __construct(

public static function fromServers(Server ...$servers): self
{
$filteredServers = array_filter($servers, fn($s) => !$s->isEmpty());
usort($servers, fn(Server $a, Server $b) => strlen($b->regex) <=> strlen($a->regex));
usort(
$filteredServers,
$servers,
fn(Server $a, Server $b) => $a->howManyDynamicComponents() <=> $b->howManyDynamicComponents()
);

$hostedServers = $hostlessServers = [];
foreach ($filteredServers as $server) {
foreach ($servers as $server) {
if ($server->isHosted()) {
$hostedServers[$server->url] = $server;
} else {
Expand Down
76 changes: 21 additions & 55 deletions src/RouteCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,78 +8,44 @@
use cebe\openapi\spec\Operation;
use cebe\openapi\spec\PathItem;
use Membrane\OpenAPIRouter\Exception\CannotCollectRoutes;
use Membrane\OpenAPIRouter\Route\Route;
use Membrane\OpenAPIRouter\Route\Server as ServerRoute;
use Membrane\OpenAPIRouter\Route\Server;

class RouteCollector
{
public function collect(OpenApi $openApi): RouteCollection
{
$collection = $this->collectRoutes($openApi);

if ($collection === []) {
throw CannotCollectRoutes::noRoutes();
}

return RouteCollection::fromServers(...$collection);
}

/** @return array<string, ServerRoute> */
private function collectRoutes(OpenApi $openApi): array
{
$collection = [];

$rootServers = $this->getServers($openApi);
foreach ($rootServers as $url => $regex) {
$collection[$url] ??= new ServerRoute($url, $regex);
}

$operationIds = [];
foreach ($openApi->paths as $path => $pathObject) {
$pathRegex = $this->getRegex($path);

$pathServers = $this->getServers($pathObject);
foreach ($pathServers as $url => $regex) {
$collection[$url] ??= new ServerRoute($url, $regex);
}

foreach ($pathObject->getOperations() as $method => $operationObject) {
$operationServers = $this->getServers($operationObject);
foreach ($operationServers as $url => $regex) {
$collection[$url] ??= new ServerRoute($url, $regex);
$servers = $this->getServers($openApi, $pathObject, $operationObject);
foreach ($servers as $serverUrl) {
if (!isset($collection[$serverUrl])) {
$collection[$serverUrl] = new Server($serverUrl);
}
$collection[$serverUrl]->addRoute($path, $method, $operationObject->operationId);
}

if ($operationServers !== []) {
$servers = $operationServers;
} elseif ($pathServers !== []) {
$servers = $pathServers;
} else {
$servers = $rootServers;
}

foreach ($servers as $url => $regex) {
$collection[$url]->addRoute(new Route($path, $pathRegex, $method, $operationObject->operationId));
}

$operationIds[$operationObject->operationId] = ['path' => $path, 'operation' => $method];
}
}

return array_filter($collection, fn($s) => !$s->isEmpty());
}
if ($collection === []) {
throw CannotCollectRoutes::noRoutes();
}

/** @return array<string, string> */
private function getServers(OpenApi|PathItem|Operation $object): array
{
$uniqueServers = array_unique(array_map(fn($p) => rtrim($p->url, '/'), $object->servers));
return array_combine($uniqueServers, array_map(fn($p) => $this->getRegex($p), $uniqueServers));
return RouteCollection::fromServers(...$collection);
}

private function getRegex(string $path): string
/** @return array<string, string> */
private function getServers(OpenApi $openAPI, PathItem $path, Operation $operation): array
{
$regex = preg_replace('#{[^/]+}#', '([^/]+)', $path);
assert($regex !== null); // The pattern is hardcoded, valid regex so should not cause an error in preg_replace
if ($operation->servers !== []) {
$servers = $operation->servers;
} elseif ($path->servers !== []) {
$servers = $path->servers;
} else {
$servers = $openAPI->servers;
}

return $regex;
return array_map(fn($p) => rtrim($p->url, '/'), $servers);
}
}
2 changes: 0 additions & 2 deletions src/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ private function routeServer(array $servers, string $url, string $method): ?stri
{
// Check static servers first
$staticServers = $servers['static'];
// Prioritize server names of greater length
uksort($staticServers, fn($a, $b) => strlen($a) <=> strlen($b));

foreach ($staticServers as $staticServer => $paths) {
if (str_starts_with($url, $staticServer)) {
Expand Down
2 changes: 1 addition & 1 deletion tests/Console/Command/CacheOpenAPIRoutesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
#[CoversClass(CacheOpenAPIRoutes::class)]
#[CoversClass(Exception\CannotCollectRoutes::class)]
#[UsesClass(Service\CacheOpenAPIRoutes::class)]
#[UsesClass(Route\Route::class), UsesClass(Route\Server::class), UsesClass(Route\Path::class)]
#[UsesClass(Route\Server::class), UsesClass(Route\Path::class)]
#[UsesClass(RouteCollection::class)]
#[UsesClass(RouteCollector::class)]
class CacheOpenAPIRoutesTest extends TestCase
Expand Down
2 changes: 1 addition & 1 deletion tests/Console/Service/CacheOpenAPIRoutesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
#[CoversClass(CacheOpenAPIRoutes::class)]
#[CoversClass(CannotCollectRoutes::class)]
#[UsesClass(RouteCollector::class)]
#[UsesClass(Route\Route::class), UsesClass(Route\Server::class), UsesClass(Route\Path::class)]
#[UsesClass(Route\Server::class), UsesClass(Route\Path::class)]
#[UsesClass(RouteCollection::class)]
class CacheOpenAPIRoutesTest extends TestCase
{
Expand Down
32 changes: 32 additions & 0 deletions tests/Exception/CannotRouteRequestTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Membrane\OpenAPIRouter\Tests\Exception;

use Generator;
use Membrane\OpenAPIRouter\Exception\CannotRouteRequest;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

#[CoversClass(CannotRouteRequest::class)]
class CannotRouteRequestTest extends TestCase
{
public static function provideErrorCodes(): Generator
{
yield '404' => [404, CannotRouteRequest::notFound()];
yield '405' => [405, CannotRouteRequest::methodNotAllowed()];
}

#[Test]
#[DataProvider('provideErrorCodes')]
public function itConstructsFromErrorCodes(int $errorCode, CannotRouteRequest $expectedException): void
{
$actualException = CannotRouteRequest::fromErrorCode($errorCode);

self::assertEquals($expectedException, CannotRouteRequest::fromErrorCode($errorCode));
self::assertSame($errorCode, $actualException->getCode());
}
}
Loading

0 comments on commit 698a6aa

Please sign in to comment.