diff --git a/phpstan.neon b/phpstan.neon index c158993..7ec9299 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,8 @@ parameters: - level: 9 - paths: - - src + typeAliases: + RouteInfo: 'array{operationId: string, source: string}' + PathInfo: 'array' + Paths: 'array' + level: 9 + paths: + - src diff --git a/src/Console/Command/CacheOpenAPIRoutes.php b/src/Console/Command/CacheOpenAPIRoutes.php index 1a44dc6..c3b2e6d 100644 --- a/src/Console/Command/CacheOpenAPIRoutes.php +++ b/src/Console/Command/CacheOpenAPIRoutes.php @@ -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 diff --git a/src/Console/Service/CacheOpenAPIRoutes.php b/src/Console/Service/CacheOpenAPIRoutes.php index 593f49d..5c3bca3 100644 --- a/src/Console/Service/CacheOpenAPIRoutes.php +++ b/src/Console/Service/CacheOpenAPIRoutes.php @@ -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 @@ -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; diff --git a/src/Route/Path.php b/src/Route/Path.php index fca88db..f7a36d9 100644 --- a/src/Route/Path.php +++ b/src/Route/Path.php @@ -9,7 +9,7 @@ final class Path implements JsonSerializable { public readonly string $regex; - /** @var array */ + /** @var array */ private array $operations = []; public function __construct( @@ -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 @@ -40,7 +40,7 @@ public function isEmpty(): bool return count($this->operations) === 0; } - /** @return array */ + /** @return array */ public function jsonSerialize(): array { $operations = $this->operations; diff --git a/src/Route/Server.php b/src/Route/Server.php index dd54f99..897cb1d 100644 --- a/src/Route/Server.php +++ b/src/Route/Server.php @@ -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)); @@ -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 @@ -51,9 +52,12 @@ public function isHosted(): bool } /** @return array{ - * 'static': array>, - * 'dynamic': array{'regex': string, 'paths': array>} - * } + * 'static': array>, + * 'dynamic': array{ + * 'regex': string, + * 'paths': array> + * } + * } */ public function jsonSerialize(): array { diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 90d305c..80d818a 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -12,27 +12,27 @@ final class RouteCollection * @param array{ * 'hosted' : array{ * 'static': array, * 'dynamic': array{ * 'regex': string, * 'servers': array * } * }, * 'hostless' : array{ * 'static': array, * 'dynamic': array{ * 'regex': string, * 'servers': array * } * } diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 79abd8d..0d804a5 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -10,6 +10,32 @@ class RouteCollector { + /** @param array $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 = []; @@ -17,7 +43,7 @@ public function collect(V30\OpenAPI|V31\OpenAPI $openApi): RouteCollection 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, diff --git a/src/RouteMatch.php b/src/RouteMatch.php new file mode 100644 index 0000000..a34311d --- /dev/null +++ b/src/RouteMatch.php @@ -0,0 +1,14 @@ +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, * 'dynamic': array{ * 'regex': string, * 'servers': array * } * } $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']; @@ -82,10 +89,10 @@ private function routeServer(array $servers, string $url, string $method): ?stri } /** @param array{ - * 'static': array>, - * 'dynamic': array{'regex': string, 'paths': array>} + * 'static': Paths, + * 'dynamic': array{'regex': string, 'paths': Paths} * } $paths - * @return string[] + * @return ?PathInfo */ private function routePath(string $server, array $paths, string $url): ?array { @@ -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]; diff --git a/tests/Console/Service/CacheOpenAPIRoutesTest.php b/tests/Console/Service/CacheOpenAPIRoutesTest.php index efba087..03ffab3 100644 --- a/tests/Console/Service/CacheOpenAPIRoutesTest.php +++ b/tests/Console/Service/CacheOpenAPIRoutesTest.php @@ -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] @@ -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] @@ -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] @@ -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)); @@ -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)); diff --git a/tests/Route/PathTest.php b/tests/Route/PathTest.php index ed0f034..5d48bd5 100644 --- a/tests/Route/PathTest.php +++ b/tests/Route/PathTest.php @@ -69,7 +69,7 @@ public function itCanAddRoutes(): void self::assertTrue($sut->isEmpty()); - $sut->addRoute('get', 'get-operation-id'); + $sut->addRoute('get', 'get-operation-id', ''); self::assertFalse($sut->isEmpty()); } @@ -77,9 +77,9 @@ public function itCanAddRoutes(): void public static function providePathsToJsonSerialize(): Generator { $expected = [ - 'delete' => 'delete-operation', - 'get' => 'get-operation', - 'post' => 'post-operation' + 'delete' => ['operationId' => 'delete-operation', 'source' => ''], + 'get' => ['operationId' => 'get-operation', 'source' => ''], + 'post' => ['operationId' => 'post-operation', 'source' => ''], ]; $operations = [ @@ -90,7 +90,7 @@ public static function providePathsToJsonSerialize(): Generator yield [ $expected, - (function() use ($operations) { + (function () use ($operations) { $sut = new Path('/path'); foreach ($operations as $operation) { $sut->addRoute(...$operation); @@ -100,7 +100,7 @@ public static function providePathsToJsonSerialize(): Generator ]; yield [ $expected, - (function() use ($operations) { + (function () use ($operations) { $sut = new Path('/path'); foreach (array_reverse($operations) as $operation) { $sut->addRoute(...$operation); @@ -115,9 +115,15 @@ public function itIsJsonSerializable(): void { $sut = new Path('/path'); - $sut->addRoute('get', 'get-operation-id'); - $sut->addRoute('post', 'post-operation-id'); + $sut->addRoute('get', 'get-operation-id', ''); + $sut->addRoute('post', 'post-operation-id', ''); - self::assertSame(['get' => 'get-operation-id', 'post' => 'post-operation-id'], $sut->jsonSerialize()); + self::assertSame( + [ + 'get' => ['operationId' => 'get-operation-id', 'source' => ''], + 'post' => ['operationId' => 'post-operation-id', 'source' => ''], + ], + $sut->jsonSerialize() + ); } } diff --git a/tests/Route/ServerTest.php b/tests/Route/ServerTest.php index e306d33..0ea1824 100644 --- a/tests/Route/ServerTest.php +++ b/tests/Route/ServerTest.php @@ -94,12 +94,17 @@ public function itCanTellIfItIsHosted(bool $expected, string $url): void public static function provideServersToJsonSerialize(): Generator { $expected = [ - 'static' => ['/path' => ['get' => 'get-path', 'post' => 'post-path']], + 'static' => ['/path' => [ + 'get' => ['operationId' => 'get-path', 'source' => ''], + 'post' => ['operationId' => 'post-path', 'source' => '']] + ], 'dynamic' => [ 'regex' => '#^(?|/([^/]+)/path(*MARK:/{another}/path)|/([^/]+)/([^/]+)/path(*MARK:/{yet}/{another}/path))$#', 'paths' => [ - '/{another}/path' => ['get' => 'get-another-path'], - '/{yet}/{another}/path' => ['delete' => 'delete-yet-another-path'], + '/{another}/path' => ['get' => ['operationId' => 'get-another-path', 'source' => '']], + '/{yet}/{another}/path' => [ + 'delete' => ['operationId' => 'delete-yet-another-path', 'source' => ''] + ], ], ], ]; diff --git a/tests/RouteCollectionTest.php b/tests/RouteCollectionTest.php index 6999f32..8759114 100644 --- a/tests/RouteCollectionTest.php +++ b/tests/RouteCollectionTest.php @@ -24,13 +24,13 @@ public static function provideServers(): Generator 'static' => [ 'https://www.server.io' => [ 'static' => [ - '/static/path' => ['post' => 'post-static-path'], + '/static/path' => ['post' => ['operationId' => 'post-static-path', 'source' => '']], ], 'dynamic' => [ 'regex' => '#^(?|/([^/]+)/path(*MARK:/{dynamic}/path)|/([^/]+)/([^/]+)/path(*MARK:/{very}/{dynamic}/path))$#', 'paths' => [ - '/{dynamic}/path' => ['get' => 'get-dynamic-path'], - '/{very}/{dynamic}/path' => ['patch' => 'patch-very-dynamic-path'] + '/{dynamic}/path' => ['get' => ['operationId' => 'get-dynamic-path', 'source' => '']], + '/{very}/{dynamic}/path' => ['patch' => ['operationId' => 'patch-very-dynamic-path', 'source' => '']] ] ] ] @@ -42,14 +42,14 @@ public static function provideServers(): Generator 'https://www.server.net/{version}' => [ 'static' => [ '/static/path' => [ - 'delete' => 'delete-static-path', - 'get' => 'get-static-path', + 'delete' => ['operationId' => 'delete-static-path', 'source' => ''], + 'get' => ['operationId' => 'get-static-path', 'source' => ''], ] ], 'dynamic' => [ 'regex' => '#^(?|/([^/]+)/path(*MARK:/{dynamic}/path))$#', 'paths' => [ - '/{dynamic}/path' => ['post' => 'post-dynamic-path'] + '/{dynamic}/path' => ['post' => ['operationId' => 'post-dynamic-path', 'source' => '']] ], ] ], @@ -58,7 +58,7 @@ public static function provideServers(): Generator 'dynamic' => [ 'regex' => '#^(?|/([^/]+)/([^/]+)/([^/]+)(*MARK:/{very}/{dynamic}/{path}}))$#', 'paths' => [ - '/{very}/{dynamic}/{path}}' => ['post' => 'post-very-dynamic-path'] + '/{very}/{dynamic}/{path}}' => ['post' => ['operationId' => 'post-very-dynamic-path', 'source' => '']] ], ] ], @@ -72,7 +72,7 @@ public static function provideServers(): Generator 'dynamic' => [ 'regex' => '#^(?|/([^/]+)/([^/]+)/([^/]+)(*MARK:/{very}/{dynamic}/{path}}))$#', 'paths' => [ - '/{very}/{dynamic}/{path}}' => ['get' => 'get-very-dynamic-path'] + '/{very}/{dynamic}/{path}}' => ['get' => ['operationId' => 'get-very-dynamic-path', 'source' => '']] ] ] ], diff --git a/tests/RouteCollectorTest.php b/tests/RouteCollectorTest.php index a9c9f5e..e504783 100644 --- a/tests/RouteCollectorTest.php +++ b/tests/RouteCollectorTest.php @@ -13,6 +13,8 @@ use Membrane\OpenAPIRouter\RouteCollection; use Membrane\OpenAPIRouter\RouteCollector; use Membrane\OpenAPIRouter\Tests\Fixtures\ProvidesApiAndRoutes; +use Membrane\OpenAPIRouter\Tests\Fixtures\ProvidesPetstoreExpanded; +use Membrane\OpenAPIRouter\Tests\Fixtures\ProvidesTrainTravel; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DataProviderExternal; @@ -72,6 +74,124 @@ public function collectTest(string $apiFilePath, RouteCollection $expected): voi OpenAPIVersion::Version_3_1, ]))->readFromAbsoluteFilePath($apiFilePath); - self::assertEquals($expected, (new RouteCollector())->collect($openAPI)); + self::assertEquals($expected, new RouteCollector()->collect($openAPI)); + } + + #[Test] + public function collectManyTest(): void + { + + + $expected = new RouteCollection( + [ + 'hosted' => [ + 'static' => [ + 'http://petstore.swagger.io/api' => [ + 'static' => [ + '/pets' => [ + 'get' => [ + 'operationId' => 'findPets', + 'source' => ProvidesPetstoreExpanded::getFilePath(), + ], + 'post' => [ + 'operationId' => 'addPet', + 'source' => ProvidesPetstoreExpanded::getFilePath(), + ], + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|/pets/([^/]+)(*MARK:/pets/{id}))$#', + 'paths' => [ + '/pets/{id}' => [ + 'delete' => [ + 'operationId' => 'deletePet', + 'source' => ProvidesPetstoreExpanded::getFilePath(), + ], + 'get' => [ + 'operationId' => 'find pet by id', + 'source' => ProvidesPetstoreExpanded::getFilePath(), + ], + ], + ], + ], + ], + 'https://api.example.com' => [ + 'static' => [ + '/stations' => [ + 'get' => [ + 'operationId' => 'get-stations', + 'source' => ProvidesTrainTravel::getFilePath(), + ], + ], + '/trips' => [ + 'get' => [ + 'operationId' => 'get-trips', + 'source' => ProvidesTrainTravel::getFilePath(), + ], + ], + '/bookings' => [ + 'get' => [ + 'operationId' => 'get-bookings', + 'source' => ProvidesTrainTravel::getFilePath(), + ], + 'post' => [ + 'operationId' => 'create-booking', + 'source' => ProvidesTrainTravel::getFilePath(), + ], + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|/bookings/([^/]+)(*MARK:/bookings/{bookingId})|/bookings/([^/]+)/payment(*MARK:/bookings/{bookingId}/payment))$#', + 'paths' => [ + '/bookings/{bookingId}' => [ + 'delete' => [ + 'operationId' => 'delete-booking', + 'source' => ProvidesTrainTravel::getFilePath(), + ], + 'get' => [ + 'operationId' => 'get-booking', + 'source' => ProvidesTrainTravel::getFilePath(), + ], + ], + '/bookings/{bookingId}/payment' => [ + 'post' => [ + 'operationId' => 'create-booking-payment', + 'source' => ProvidesTrainTravel::getFilePath(), + ], + ], + ], + ], + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|)#', + 'servers' => [ + ], + ], + ], + 'hostless' => [ + 'static' => [ + ], + 'dynamic' => [ + 'regex' => '#^(?|)#', + 'servers' => [ + ], + ], + ], + ] + ); + + $specs = $collection = []; + + foreach ([ProvidesPetstoreExpanded::class, ProvidesTrainTravel::class] as $class) { + $filePath = $class::getFilePath(); + $specs[$filePath] = new MembraneReader([ + OpenAPIVersion::Version_3_0, + OpenAPIVersion::Version_3_1, + ])->readFromAbsoluteFilePath($filePath); + } + + + self::assertEquals($expected, (new RouteCollector())->collectMany($specs)); } } diff --git a/tests/RouterTest.php b/tests/RouterTest.php index d8a3ac0..7ba217f 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -36,13 +36,13 @@ public static function unsuccessfulRouteProvider(): array Exception\CannotRouteRequest::notFound(), 'https://hatshop.dapper.net/api/pets', 'get', - ProvidesPetstoreExpanded::getRoutes(), + ProvidesPetstoreExpanded::getRouteCollection(), ], 'petstore-expanded: correct static server url but incorrect path' => [ Exception\CannotRouteRequest::notFound(), 'http://petstore.swagger.io/api/hats', 'get', - ProvidesPetstoreExpanded::getRoutes(), + ProvidesPetstoreExpanded::getRouteCollection(), ], 'WeirdAndWonderful: correct dynamic erver url but incorrect path' => [ Exception\CannotRouteRequest::notFound(), @@ -54,7 +54,7 @@ public static function unsuccessfulRouteProvider(): array Exception\CannotRouteRequest::methodNotAllowed(), 'http://petstore.swagger.io/api/pets', 'delete', - ProvidesPetstoreExpanded::getRoutes(), + ProvidesPetstoreExpanded::getRouteCollection(), ], ]; } @@ -81,19 +81,19 @@ public static function successfulRouteProvider(): array 'findPets', 'http://petstore.swagger.io/api/pets', 'get', - ProvidesPetstoreExpanded::getRoutes(), + ProvidesPetstoreExpanded::getRouteCollection(), ], 'petstore: /pets/{id} path, get method' => [ 'find pet by id', 'http://petstore.swagger.io/api/pets/1', 'get', - ProvidesPetstoreExpanded::getRoutes(), + ProvidesPetstoreExpanded::getRouteCollection(), ], 'petstore: /pets/{id} path, delete method' => [ 'deletePet', 'http://petstore.swagger.io/api/pets/1', 'delete', - ProvidesPetstoreExpanded::getRoutes(), + ProvidesPetstoreExpanded::getRouteCollection(), ], 'WeirdAndWonderful: /v1/or path, post method' => [ 'post-or', @@ -290,4 +290,21 @@ public function itPrioritisesServersCorrectly(string $url, string $openAPI): voi self::assertSame('first', $priority); } + + #[Test, TestDox('Servers are prioritised by length and number of dynamic components')] + #[DataProvider('provideServersToPrioritise')] + public function testMatch(string $url, string $openAPI): void + { + $openAPI = (new MembraneReader([OpenAPIVersion::Version_3_0])) + ->readFromString($openAPI, FileFormat::Json); + + $routeCollection = (new RouteCollector()) + ->collectMany(['source.json' => $openAPI]); + + $routeMatch = (new Router($routeCollection)) + ->match($url, 'get'); + + self::assertSame('first', $routeMatch->operationId); + self::assertSame('source.json', $routeMatch->source); + } } diff --git a/tests/fixtures/ProvidesAPIeceOfCake.php b/tests/fixtures/ProvidesAPIeceOfCake.php index ec258f8..7383ad2 100644 --- a/tests/fixtures/ProvidesAPIeceOfCake.php +++ b/tests/fixtures/ProvidesAPIeceOfCake.php @@ -24,22 +24,22 @@ public static function getRoutes(): RouteCollection 'static' => ['' => [ 'static' => [ '/cakes/sponge' => [ - 'get' => 'findSpongeCakes', + 'get' => ['operationId' => 'findSpongeCakes', 'source' => ''], ] ], 'dynamic' => [ 'regex' => '#^(?|/cakes/([^/]+)(*MARK:/cakes/{icing})|/([^/]+)/sponge(*MARK:/{cakeType}/sponge)|/([^/]+)/([^/]+)(*MARK:/{cakeType}/{icing}))$#', 'paths' => [ '/cakes/{icing}' => [ - 'get' => 'findCakesByIcing', - 'post' => 'addCakesByIcing', + 'get' => ['operationId' => 'findCakesByIcing', 'source' => ''], + 'post' => ['operationId' => 'addCakesByIcing', 'source' => ''], ], '/{cakeType}/sponge' => [ - 'get' => 'findSpongeByDesserts', + 'get' => ['operationId' => 'findSpongeByDesserts', 'source' => ''], ], '/{cakeType}/{icing}' => [ - 'get' => 'findDessertByIcing', - 'post' => 'addDessertByIcing', + 'get' => ['operationId' => 'findDessertByIcing', 'source' => ''], + 'post' => ['operationId' => 'addDessertByIcing', 'source' => ''], ], ] ], @@ -60,22 +60,22 @@ public static function getRoutesIgnoringServers(): RouteCollection 'static' => ['' => [ 'static' => [ '/cakes/sponge' => [ - 'get' => 'findSpongeCakes', + 'get' => ['operationId' => 'findSpongeCakes', 'source' => ''], ] ], 'dynamic' => [ 'regex' => '#^(?|/cakes/([^/]+)(*MARK:/cakes/{icing})|/([^/]+)/sponge(*MARK:/{cakeType}/sponge)|/([^/]+)/([^/]+)(*MARK:/{cakeType}/{icing}))$#', 'paths' => [ '/cakes/{icing}' => [ - 'get' => 'findCakesByIcing', - 'post' => 'addCakesByIcing', + 'get' => ['operationId' => 'findCakesByIcing', 'source' => ''], + 'post' => ['operationId' => 'addCakesByIcing', 'source' => ''], ], '/{cakeType}/sponge' => [ - 'get' => 'findSpongeByDesserts', + 'get' => ['operationId' => 'findSpongeByDesserts', 'source' => ''], ], '/{cakeType}/{icing}' => [ - 'get' => 'findDessertByIcing', - 'post' => 'addDessertByIcing', + 'get' => ['operationId' => 'findDessertByIcing', 'source' => ''], + 'post' => ['operationId' => 'addDessertByIcing', 'source' => ''], ], ] ], diff --git a/tests/fixtures/ProvidesApiAndRoutes.php b/tests/fixtures/ProvidesApiAndRoutes.php index 5ae5eee..9fb2f24 100644 --- a/tests/fixtures/ProvidesApiAndRoutes.php +++ b/tests/fixtures/ProvidesApiAndRoutes.php @@ -14,12 +14,12 @@ public static function defaultBehaviour(): Generator { yield 'petstore-expanded' => [ ProvidesPetstoreExpanded::getFilePath(), - ProvidesPetstoreExpanded::getRoutes(), + ProvidesPetstoreExpanded::getRouteCollection(), ]; yield 'train-travel' => [ ProvidesTrainTravel::getFilePath(), - ProvidesTrainTravel::getRoutes() + ProvidesTrainTravel::getRouteCollection() ]; yield 'APIece of Cake' => [ diff --git a/tests/fixtures/ProvidesPetstoreExpanded.php b/tests/fixtures/ProvidesPetstoreExpanded.php index e4d009a..e2365bd 100644 --- a/tests/fixtures/ProvidesPetstoreExpanded.php +++ b/tests/fixtures/ProvidesPetstoreExpanded.php @@ -14,45 +14,62 @@ public static function getFilePath(): string return __DIR__ . '/docs/petstore-expanded.json'; } - public static function getRoutes(): RouteCollection + public static function getRouteCollection(): RouteCollection + { + return new RouteCollection(self::getRoutes()); + } + + public static function getRoutesIgnoringServers(): RouteCollection { return new RouteCollection([ - 'hosted' => [ - 'static' => ['http://petstore.swagger.io/api' => [ + 'hosted' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], + 'hostless' => [ + 'static' => ['' => [ 'static' => [ - '/pets' => ['get' => 'findPets', 'post' => 'addPet'], + '/pets' => [ + 'get' => ['operationId' => 'findPets', 'source' => ''], + 'post' => ['operationId' => 'addPet', 'source' => ''], + ], ], 'dynamic' => [ 'regex' => '#^(?|/pets/([^/]+)(*MARK:/pets/{id}))$#', 'paths' => [ - '/pets/{id}' => ['get' => 'find pet by id', 'delete' => 'deletePet'], + '/pets/{id}' => [ + 'get' => ['operationId' => 'find pet by id', 'source' => ''], + 'delete' => ['operationId' => 'deletePet', 'source' => ''], + ], ], ], ]], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []], ], - 'hostless' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], ]); } - public static function getRoutesIgnoringServers(): RouteCollection + public static function getRoutes($source = ''): array { - return new RouteCollection([ - 'hosted' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], - 'hostless' => [ - 'static' => ['' => [ + return [ + 'hosted' => [ + 'static' => ['http://petstore.swagger.io/api' => [ 'static' => [ - '/pets' => ['get' => 'findPets', 'post' => 'addPet'], + '/pets' => [ + 'get' => ['operationId' => 'findPets', 'source' => $source], + 'post' => ['operationId' => 'addPet', 'source' => $source] + ], ], 'dynamic' => [ 'regex' => '#^(?|/pets/([^/]+)(*MARK:/pets/{id}))$#', 'paths' => [ - '/pets/{id}' => ['get' => 'find pet by id', 'delete' => 'deletePet'], + '/pets/{id}' => [ + 'get' => ['operationId' => 'find pet by id', 'source' => $source], + 'delete' => ['operationId' => 'deletePet', 'source' => $source] + ], ], ], ]], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []], ], - ]); + 'hostless' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], + ]; } } diff --git a/tests/fixtures/ProvidesTrainTravel.php b/tests/fixtures/ProvidesTrainTravel.php index 52b24b0..12e93df 100644 --- a/tests/fixtures/ProvidesTrainTravel.php +++ b/tests/fixtures/ProvidesTrainTravel.php @@ -14,51 +14,72 @@ public static function getFilePath(): string return __DIR__ . '/train-travel-api.yaml'; } - public static function getRoutes(): RouteCollection + public static function getRouteCollection(): RouteCollection { - return new RouteCollection([ - 'hosted' => [ - 'static' => ['https://api.example.com' => [ + return new RouteCollection(self::getRoutes()); + } + + public static function getRoutesIgnoringServers(): RouteCollection + { + return new RouteCollection([ + 'hosted' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], + 'hostless' => [ + 'static' => ['' => [ 'static' => [ - '/stations' => ['get' => 'get-stations'], - '/trips' => ['get' => 'get-trips'], - '/bookings' => ['get' => 'get-bookings', 'post' => 'create-booking'], + '/stations' => ['get' => ['operationId' => 'get-stations', 'source' => '']], + '/trips' => ['get' => ['operationId' => 'get-trips', 'source' => '']], + '/bookings' => [ + 'get' => ['operationId' => 'get-bookings', 'source' => ''], + 'post' => ['operationId' => 'create-booking', 'source' => ''] + ], ], 'dynamic' => [ 'regex' => '#^(?|/bookings/([^/]+)(*MARK:/bookings/{bookingId})|/bookings/([^/]+)/payment(*MARK:/bookings/{bookingId}/payment))$#', 'paths' => [ - '/bookings/{bookingId}' => ['get' => 'get-booking', 'delete' => 'delete-booking'], - '/bookings/{bookingId}/payment' => ['post' => 'create-booking-payment'], + '/bookings/{bookingId}' => [ + 'get' => ['operationId' => 'get-booking', 'source' => ''], + 'delete' => ['operationId' => 'delete-booking', 'source' => ''] + ], + '/bookings/{bookingId}/payment' => [ + 'post' => ['operationId' => 'create-booking-payment', 'source' => ''] + ], ], ], ]], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []], ], - 'hostless' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], ]); } - public static function getRoutesIgnoringServers(): RouteCollection + public static function getRoutes(string $source = ''): array { - return new RouteCollection([ - 'hosted' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], - 'hostless' => [ - 'static' => ['' => [ + return [ + 'hosted' => [ + 'static' => ['https://api.example.com' => [ 'static' => [ - '/stations' => ['get' => 'get-stations'], - '/trips' => ['get' => 'get-trips'], - '/bookings' => ['get' => 'get-bookings', 'post' => 'create-booking'], + '/stations' => ['get' => ['operationId' => 'get-stations', 'source' => $source]], + '/trips' => ['get' => ['operationId' => 'get-trips', 'source' => $source]], + '/bookings' => [ + 'get' => ['operationId' => 'get-bookings', 'source' => $source], + 'post' => ['operationId' => 'create-booking', 'source' => $source], + ], ], 'dynamic' => [ 'regex' => '#^(?|/bookings/([^/]+)(*MARK:/bookings/{bookingId})|/bookings/([^/]+)/payment(*MARK:/bookings/{bookingId}/payment))$#', 'paths' => [ - '/bookings/{bookingId}' => ['get' => 'get-booking', 'delete' => 'delete-booking'], - '/bookings/{bookingId}/payment' => ['post' => 'create-booking-payment'], + '/bookings/{bookingId}' => [ + 'get' => ['operationId' => 'get-booking', 'source' => $source], + 'delete' => ['operationId' => 'delete-booking', 'source' => $source], + ], + '/bookings/{bookingId}/payment' => [ + 'post' => ['operationId' => 'create-booking-payment', 'source' => $source], + ], ], ], ]], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []], ], - ]); + 'hostless' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], + ]; } } diff --git a/tests/fixtures/ProvidesWeirdAndWonderful.php b/tests/fixtures/ProvidesWeirdAndWonderful.php index ce33097..6af63ca 100644 --- a/tests/fixtures/ProvidesWeirdAndWonderful.php +++ b/tests/fixtures/ProvidesWeirdAndWonderful.php @@ -21,37 +21,51 @@ public static function getRoutes(): RouteCollection 'static' => [ 'http://weirdest.com' => [ 'static' => [ - '/however' => ['put' => 'put-however', 'post' => 'post-however'], + '/however' => [ + 'put' => ['operationId' => 'put-however', 'source' => ''], + 'post' => ['operationId' => 'post-however', 'source' => ''], + ], ], 'dynamic' => [ 'regex' => '#^(?|/and/([^/]+)(*MARK:/and/{name}))$#', 'paths' => [ - '/and/{name}' => ['get' => 'get-and'] + '/and/{name}' => [ + 'get' => ['operationId' => 'get-and', 'source' => ''] + ] ], ], ], 'http://weirder.co.uk' => [ 'static' => [ - '/however' => ['get' => 'get-however'] + '/however' => [ + 'get' => ['operationId' => 'get-however', 'source' => ''] + ] ], 'dynamic' => [ 'regex' => '#^(?|/and/([^/]+)(*MARK:/and/{name}))$#', 'paths' => [ - '/and/{name}' => ['put' => 'put-and', 'post' => 'post-and'], + '/and/{name}' => [ + 'put' => ['operationId' => 'put-and', 'source' => ''], + 'post' => ['operationId' => 'post-and', 'source' => ''] + ], ], ], ], 'http://wonderful.io' => [ 'static' => [ - '/or' => ['post' => 'post-or'], - '/xor' => ['delete' => 'delete-xor'], + '/or' => [ + 'post' => ['operationId' => 'post-or', 'source' => ''] + ], + '/xor' => [ + 'delete' => ['operationId' => 'delete-xor', 'source' => ''] + ], ], 'dynamic' => ['regex' => '#^(?|)$#', 'paths' => []], ], 'http://wonderful.io/and' => [ 'static' => [ - '/or' => ['post' => 'post-or'], - '/xor' => ['delete' => 'delete-xor'], + '/or' => ['post' => ['operationId' => 'post-or', 'source' => '']], + '/xor' => ['delete' => ['operationId' => 'delete-xor', 'source' => '']], ], 'dynamic' => [ 'regex' => '#^(?|)$#', @@ -60,8 +74,8 @@ public static function getRoutes(): RouteCollection ], 'http://wonderful.io/or' => [ 'static' => [ - '/or' => ['post' => 'post-or'], - '/xor' => ['delete' => 'delete-xor'], + '/or' => ['post' => ['operationId' => 'post-or', 'source' => '']], + '/xor' => ['delete' => ['operationId' => 'delete-xor', 'source' => '']], ], 'dynamic' => ['regex' => '#^(?|)$#', 'paths' => []], ], @@ -70,8 +84,8 @@ public static function getRoutes(): RouteCollection 'regex' => '#^(?|http://weird.io/([^/]+)(*MARK:http://weird.io/{conjunction}))#', 'servers' => ['http://weird.io/{conjunction}' => [ 'static' => [ - '/or' => ['post' => 'post-or'], - '/xor' => ['delete' => 'delete-xor'], + '/or' => ['post' => ['operationId' => 'post-or', 'source' => '']], + '/xor' => ['delete' => ['operationId' => 'delete-xor', 'source' => '']], ], 'dynamic' => ['regex' => '#^(?|)$#', 'paths' => []], ]], @@ -81,15 +95,15 @@ public static function getRoutes(): RouteCollection 'static' => [ '' => [ 'static' => [ - '/or' => ['post' => 'post-or'], - '/xor' => ['delete' => 'delete-xor'], + '/or' => ['post' => ['operationId' => 'post-or', 'source' => '']], + '/xor' => ['delete' => ['operationId' => 'delete-xor', 'source' => '']], ], 'dynamic' => ['regex' => '#^(?|)$#', 'paths' => []], ], '/v1' => [ 'static' => [ - '/or' => ['post' => 'post-or'], - '/xor' => ['delete' => 'delete-xor'], + '/or' => ['post' => ['operationId' => 'post-or', 'source' => '']], + '/xor' => ['delete' => ['operationId' => 'delete-xor', 'source' => '']], ], 'dynamic' => ['regex' => '#^(?|)$#', 'paths' => []], ], @@ -98,8 +112,8 @@ public static function getRoutes(): RouteCollection 'regex' => '#^(?|/([^/]+)(*MARK:/{version}))#', 'servers' => ['/{version}' => [ 'static' => [ - '/or' => ['post' => 'post-or'], - '/xor' => ['delete' => 'delete-xor'], + '/or' => ['post' => ['operationId' => 'post-or', 'source' => '']], + '/xor' => ['delete' => ['operationId' => 'delete-xor', 'source' => '']], ], 'dynamic' => ['regex' => '#^(?|)$#', 'paths' => []], ]], @@ -116,24 +130,24 @@ public static function getRoutesIgnoringServers(): RouteCollection 'static' => ['' => [ 'static' => [ '/or' => [ - 'post' => 'post-or', + 'post' => ['operationId' => 'post-or', 'source' => ''], ], '/xor' => [ - 'delete' => 'delete-xor', + 'delete' => ['operationId' => 'delete-xor', 'source' => ''], ], '/however' => [ - 'get' => 'get-however', - 'put' => 'put-however', - 'post' => 'post-however', + 'get' => ['operationId' => 'get-however', 'source' => ''], + 'put' => ['operationId' => 'put-however', 'source' => ''], + 'post' => ['operationId' => 'post-however', 'source' => ''], ], ], 'dynamic' => [ 'regex' => '#^(?|/and/([^/]+)(*MARK:/and/{name}))$#', 'paths' => [ '/and/{name}' => [ - 'get' => 'get-and', - 'put' => 'put-and', - 'post' => 'post-and', + 'get' => ['operationId' => 'get-and', 'source' => ''], + 'put' => ['operationId' => 'put-and', 'source' => ''], + 'post' => ['operationId' => 'post-and', 'source' => ''], ], ] ],