Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
parameters:
level: 9
paths:
- src
typeAliases:
RouteInfo: 'array{operationId: string, source: string}'
PathInfo: 'array<string, RouteInfo>'
Paths: 'array<string, PathInfo>'
level: 9
paths:
- src
2 changes: 1 addition & 1 deletion src/Console/Command/CacheOpenAPIRoutes.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$logger = new ConsoleLogger($output);

return (new Service\CacheOpenAPIRoutes($logger))->cache(
$openAPIFilePath,
['' => $openAPIFilePath],
$destination,
$ignoreServers,
// @todo add support this in the reader first
Expand Down
27 changes: 17 additions & 10 deletions src/Console/Service/CacheOpenAPIRoutes.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ public function __construct(
) {
}

/** @param string[] $openAPIFilePaths */
public function cache(
string $openAPIFilePath,
array $openAPIFilePaths,
string $cacheDestination,
bool $ignoreServers = false,
//@todo add support for this in the reader first
Expand All @@ -35,27 +36,33 @@ public function cache(
return false;
}

$openApis = [];

try {
$openApi = (new MembraneReader([
OpenAPIVersion::Version_3_0,
OpenAPIVersion::Version_3_1
]))->readFromAbsoluteFilePath($openAPIFilePath);
foreach ($openAPIFilePaths as $name => $openAPIFilePath) {
$openApi = new MembraneReader([
OpenAPIVersion::Version_3_0,
OpenAPIVersion::Version_3_1
])->readFromAbsoluteFilePath($openAPIFilePath);

if ($ignoreServers) {
$openApi = $openApi->withoutServers();
}

$openApis[$name] = $openApi;
}
} catch (CannotRead $e) {
$this->logger->error($e->getMessage());
return false;
}

if ($ignoreServers) {
$openApi = $openApi->withoutServers();
}

//@todo add support for this in reader first
// if ($hostlessFallback) {
// $openApi = $openApi->withHostlessFallback();
// }

try {
$routeCollection = (new RouteCollector())->collect($openApi);
$routeCollection = (new RouteCollector())->collectMany($openApis);
} catch (CannotCollectRoutes $e) {
$this->logger->error($e->getMessage());
return false;
Expand Down
8 changes: 4 additions & 4 deletions src/Route/Path.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
final class Path implements JsonSerializable
{
public readonly string $regex;
/** @var array<string, string> */
/** @var array<string, array{operationId: string, source: string}> */
private array $operations = [];

public function __construct(
Expand All @@ -20,9 +20,9 @@ public function __construct(
$this->regex = $regex;
}

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

public function isDynamic(): bool
Expand All @@ -40,7 +40,7 @@ public function isEmpty(): bool
return count($this->operations) === 0;
}

/** @return array<string, string> */
/** @return array<string, array{operationId: string, source: string}> */
public function jsonSerialize(): array
{
$operations = $this->operations;
Expand Down
14 changes: 9 additions & 5 deletions src/Route/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ final class Server implements JsonSerializable
private array $paths = [];

public function __construct(
public readonly string $url
public readonly string $url,
public readonly string $source = '',
) {
$regex = preg_replace('#{[^/]+}#', '([^/]+)', $this->url);
assert(is_string($regex));
Expand All @@ -27,7 +28,7 @@ public function addRoute(string $pathUrl, string $method, string $operationId):
$this->paths[$pathUrl] = new Path($pathUrl);
}

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

public function isDynamic(): bool
Expand All @@ -51,9 +52,12 @@ public function isHosted(): bool
}

/** @return array{
* 'static': array<string, array<string,string>>,
* 'dynamic': array{'regex': string, 'paths': array<string, array<string,string>>}
* }
* 'static': array<string, array<string, array{operationId: string, source: string}>>,
* 'dynamic': array{
* 'regex': string,
* 'paths': array<string, array<string, array{operationId: string, source: string}>>
* }
* }
*/
public function jsonSerialize(): array
{
Expand Down
16 changes: 8 additions & 8 deletions src/RouteCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,27 @@ final class RouteCollection
* @param array{
* 'hosted' : array{
* 'static': array<array{
* 'static': string[][],
* 'dynamic': array{'regex': string, 'paths': string[][]}
* 'static': Paths,
* 'dynamic': array{'regex': string, 'paths': Paths}
* }>,
* 'dynamic': array{
* 'regex': string,
* 'servers': array<array{
* 'static': string[][],
* 'dynamic': array{'regex': string, 'paths': string[][]}
* 'static': Paths,
* 'dynamic': array{'regex': string, 'paths': Paths}
* }>
* }
* },
* 'hostless' : array{
* 'static': array<array{
* 'static': string[][],
* 'dynamic': array{'regex': string, 'paths': string[][]}
* 'static': Paths,
* 'dynamic': array{'regex': string, 'paths': Paths}
* }>,
* 'dynamic': array{
* 'regex': string,
* 'servers': array<array{
* 'static': string[][],
* 'dynamic': array{'regex': string, 'paths': string[][]}
* 'static': Paths,
* 'dynamic': array{'regex': string, 'paths': Paths}
* }>
* }
* }
Expand Down
28 changes: 27 additions & 1 deletion src/RouteCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,40 @@

class RouteCollector
{
/** @param array<string, V30\OpenAPI|V31\OpenAPI> $specs */
public function collectMany(array $specs): RouteCollection
{
$collection = [];
foreach ($specs as $source => $openApi) {
foreach ($openApi->paths as $path => $pathObject) {
foreach ($pathObject->getOperations() as $method => $operation) {
foreach ($operation->servers as $server) {
$collection[$server->url] ??= new Server($server->url, $source);
$collection[$server->url]->addRoute(
$path,
$method,
$operation->operationId
);
}
}
}
}

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

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

public function collect(V30\OpenAPI|V31\OpenAPI $openApi): RouteCollection
{
$collection = [];

foreach ($openApi->paths as $path => $pathObject) {
foreach ($pathObject->getOperations() as $method => $operation) {
foreach ($operation->servers as $server) {
$collection[$server->url] ??= new Server($server->url);
$collection[$server->url] ??= new Server($server->url, '');
$collection[$server->url]->addRoute(
$path,
$method,
Expand Down
14 changes: 14 additions & 0 deletions src/RouteMatch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Membrane\OpenAPIRouter;

class RouteMatch
{
public function __construct(
public readonly string $operationId,
public readonly string $source,
) {
}
}
36 changes: 23 additions & 13 deletions src/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,47 @@ public function __construct(
) {
}

public function route(string $url, string $method): string
public function match(string $url, string $method): RouteMatch
{
$hostedMatch = $this->routeServer($this->routeCollection->routes['hosted'], $url, $method);
if ($hostedMatch !== null) {
return $hostedMatch;
return new RouteMatch(...$hostedMatch);
}

$hostlessUrl = parse_url($url, PHP_URL_PATH);
if ($hostlessUrl !== null && $hostlessUrl !== false) {
$hostlessMatch = $this->routeServer($this->routeCollection->routes['hostless'], $hostlessUrl, $method);
if ($hostlessMatch !== null) {
return $hostlessMatch;
return new RouteMatch(...$hostlessMatch);
}
}

throw CannotRouteRequest::fromErrorCode($this->errorCode);
}

/** @deprecated Use match instead for more route information */
public function route(string $url, string $method): string
{
return $this->match($url, $method)->operationId;
}

/**
* @param array{
* 'static': array<array{
* 'static': string[][],
* 'dynamic': array{'regex': string, 'paths': string[][]}
* 'static': Paths,
* 'dynamic': array{'regex': string, 'paths': Paths}
* }>,
* 'dynamic': array{
* 'regex': string,
* 'servers': array<array{
* 'static': string[][],
* 'dynamic': array{'regex': string, 'paths': string[][]}
* 'static': Paths,
* 'dynamic': array{'regex': string, 'paths': Paths}
* }>
* }
* } $servers
* @return ?RouteInfo
*/
private function routeServer(array $servers, string $url, string $method): ?string
private function routeServer(array $servers, string $url, string $method): ?array
{
// Check static servers first
$staticServers = $servers['static'];
Expand Down Expand Up @@ -82,10 +89,10 @@ private function routeServer(array $servers, string $url, string $method): ?stri
}

/** @param array{
* 'static': array<string,array<string,string>>,
* 'dynamic': array{'regex': string, 'paths': array<string,array<string,string>>}
* 'static': Paths,
* 'dynamic': array{'regex': string, 'paths': Paths}
* } $paths
* @return string[]
* @return ?PathInfo
*/
private function routePath(string $server, array $paths, string $url): ?array
{
Expand All @@ -106,8 +113,11 @@ private function routePath(string $server, array $paths, string $url): ?array
return null;
}

/** @param string[] $path */
private function routeOperation(array $path, string $method): ?string
/**
* @param PathInfo $path
* @return ?RouteInfo
*/
private function routeOperation(array $path, string $method): ?array
{
if (isset($path[$method])) {
return $path[$method];
Expand Down
10 changes: 5 additions & 5 deletions tests/Console/Service/CacheOpenAPIRoutesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function outputsErrorForReadonlyFilePaths(): void
mkdir($cache);
chmod($cache, 0444);

self::assertFalse($this->sut->cache($this->fixtures . 'docs/petstore-expanded.json', $cache));
self::assertFalse($this->sut->cache(['' => $this->fixtures . 'docs/petstore-expanded.json'], $cache));
}

#[Test]
Expand All @@ -55,7 +55,7 @@ public function cannotRouteWithoutPaths(): void
json_encode(['openapi' => '3.0.0', 'info' => ['title' => '', 'version' => '1.0.0'], 'paths' => []])
);

self::assertFalse($this->sut->cache($openAPIFilePath, $this->root . '/cache/routes.php'));
self::assertFalse($this->sut->cache(['' => $openAPIFilePath], $this->root . '/cache/routes.php'));
}

#[Test]
Expand All @@ -65,7 +65,7 @@ public function cannotRouteFromRelativeFilePaths(): void

self::assertTrue(file_exists($filePath));

self::assertFalse($this->sut->cache($filePath, $this->root . '/cache/routes.php'));
self::assertFalse($this->sut->cache(['' => $filePath], $this->root . '/cache/routes.php'));
}

#[Test]
Expand All @@ -76,7 +76,7 @@ public function itCachesRoutes(
): void {
$cachePath = vfsStream::url('root/cache/routes.php');

self::assertTrue($this->sut->cache($apiPath, $cachePath));
self::assertTrue($this->sut->cache(['' => $apiPath], $cachePath));

$actualRouteCollection = eval('?>' . file_get_contents($cachePath));

Expand All @@ -91,7 +91,7 @@ public function itCachesRoutesIgnoringServers(
): void {
$cachePath = vfsStream::url('root/cache/routes.php');

self::assertTrue($this->sut->cache($apiPath, $cachePath, true));
self::assertTrue($this->sut->cache(['' => $apiPath], $cachePath, true));

$actual = eval('?>' . file_get_contents($cachePath));

Expand Down
Loading