diff --git a/README.md b/README.md index 9cee9cb..4113848 100644 --- a/README.md +++ b/README.md @@ -5,40 +5,38 @@ To make sure it runs quickly we've used techniques inspired by [Nikita Popov](https://www.npopov.com/2014/02/18/Fast-request-routing-using-regular-expressions.html) and [Nicolas Grekas](https://nicolas-grekas.medium.com/making-symfonys-router-77-7x-faster-1-2-958e3754f0e1). -# Requirements +## Requirements - A valid [OpenAPI specification](https://github.com/OAI/OpenAPI-Specification#readme). - An operationId on all [Operation Objects](https://spec.openapis.org/oas/v3.1.0#operation-object) so that each route is uniquely identifiable. -# Rules +## Rules -## Naming Conventions +### Naming Conventions - Forward slashes at the end of a server url will be ignored since [paths MUST begin with a forward slash.](https://spec.openapis.org/oas/v3.1.0#paths-object) - [Dynamic paths which are identical other than the variable names MUST NOT exist.](https://spec.openapis.org/oas/v3.1.0#paths-object) -## Routing Priorities +### Routing Priorities - [Static urls MUST be prioritized over dynamic urls](https://spec.openapis.org/oas/v3.1.0#paths-object). - Longer urls are prioritized over shorter urls. - Hosted servers will be prioritized over hostless servers. -# Installation +## Installation ```text composer require membrane/openapi-router ``` -# Quick Start +## Quick Start To read routes dynamically, you can do the following: ```php readFromAbsoluteFilePath('/app/petstore.yaml'); $routeCollection = (new RouteCollector())->collect($openApi); @@ -49,7 +47,7 @@ $requestedOperationId = $router->route('http://petstore.swagger.io/v1/pets', 'ge echo $requestedOperationId; // listPets ``` -# Caching Routes +## Caching Routes Run the following console command to cache the routes from your OpenAPI, to avoid reading your OpenAPI file everytime: @@ -57,11 +55,10 @@ Run the following console command to cache the routes from your OpenAPI, to avoi membrane:router:generate-routes ``` - ```php readFromAbsoluteFilePath($openAPIFilePath); - } catch (CannotReadOpenAPI $e) { + $openApi = (new Reader([OpenAPIVersion::Version_3_0, OpenAPIVersion::Version_3_1])) + ->readFromAbsoluteFilePath($openAPIFilePath); + } catch (CannotRead $e) { $this->logger->error($e->getMessage()); return false; } try { $routeCollection = (new RouteCollector())->collect($openApi); - } catch (CannotRouteOpenAPI | CannotProcessOpenAPI $e) { + } catch (CannotCollectRoutes $e) { $this->logger->error($e->getMessage()); return false; } diff --git a/src/Exception/CannotRouteOpenAPI.php b/src/Exception/CannotCollectRoutes.php similarity index 82% rename from src/Exception/CannotRouteOpenAPI.php rename to src/Exception/CannotCollectRoutes.php index 51cdde5..a9bdeb4 100644 --- a/src/Exception/CannotRouteOpenAPI.php +++ b/src/Exception/CannotCollectRoutes.php @@ -6,7 +6,9 @@ /* This exception occurs when a route collection cannot be created from your OpenAPI */ -class CannotRouteOpenAPI extends \RuntimeException +use RuntimeException; + +class CannotCollectRoutes extends RuntimeException { public const NO_ROUTES = 0; diff --git a/src/Exception/CannotProcessOpenAPI.php b/src/Exception/CannotProcessOpenAPI.php deleted file mode 100644 index c36340a..0000000 --- a/src/Exception/CannotProcessOpenAPI.php +++ /dev/null @@ -1,80 +0,0 @@ -supportedFileTypes = [ - 'json' => fn($p) => Reader::readFromJsonFile(fileName: $p), - 'yaml' => fn($p) => Reader::readFromYamlFile(fileName: $p), - 'yml' => fn($p) => Reader::readFromYamlFile(fileName: $p), - ]; - } - - public function readFromAbsoluteFilePath(string $absoluteFilePath): OpenApi - { - file_exists($absoluteFilePath) ?: throw CannotReadOpenAPI::fileNotFound($absoluteFilePath); - - $fileType = strtolower(pathinfo($absoluteFilePath, PATHINFO_EXTENSION)); - - $readFrom = $this->supportedFileTypes[$fileType] ?? throw CannotReadOpenAPI::fileTypeNotSupported($fileType); - - try { - $openAPI = $readFrom($absoluteFilePath); - } catch (TypeError | TypeErrorException | ParseException $e) { - throw CannotReadOpenAPI::cannotParse(pathinfo($absoluteFilePath, PATHINFO_BASENAME), $e); - } catch (UnresolvableReferenceException $e) { - throw CannotReadOpenAPI::unresolvedReference(pathinfo($absoluteFilePath, PATHINFO_BASENAME), $e); - } - - $openAPI->validate() ?: throw CannotReadOpenAPI::invalidOpenAPI(pathinfo($absoluteFilePath, PATHINFO_BASENAME)); - - return $openAPI; - } -} diff --git a/src/Router/Route/Path.php b/src/Route/Path.php similarity index 94% rename from src/Router/Route/Path.php rename to src/Route/Path.php index 77b89fd..789a7ae 100644 --- a/src/Router/Route/Path.php +++ b/src/Route/Path.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Membrane\OpenAPIRouter\Router\Route; +namespace Membrane\OpenAPIRouter\Route; use JsonSerializable; diff --git a/src/Router/Route/Route.php b/src/Route/Route.php similarity index 84% rename from src/Router/Route/Route.php rename to src/Route/Route.php index 5547f5c..66901e5 100644 --- a/src/Router/Route/Route.php +++ b/src/Route/Route.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Membrane\OpenAPIRouter\Router\Route; +namespace Membrane\OpenAPIRouter\Route; class Route { diff --git a/src/Router/Route/Server.php b/src/Route/Server.php similarity index 91% rename from src/Router/Route/Server.php rename to src/Route/Server.php index b04ae6d..969a72f 100644 --- a/src/Router/Route/Server.php +++ b/src/Route/Server.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Membrane\OpenAPIRouter\Router\Route; +namespace Membrane\OpenAPIRouter\Route; use JsonSerializable; @@ -54,7 +54,10 @@ public function isHosted(): bool public function jsonSerialize(): array { $filteredPaths = array_filter($this->paths, fn($p) => !$p->isEmpty()); - usort($filteredPaths, fn($a, $b) => $a->howManyDynamicComponents() <=> $b->howManyDynamicComponents()); + usort( + $filteredPaths, + fn(Path $a, Path $b) => $a->howManyDynamicComponents() <=> $b->howManyDynamicComponents() + ); $staticPaths = $dynamicPaths = $regex = []; foreach ($filteredPaths as $path) { diff --git a/src/Router/RouteCollection.php b/src/RouteCollection.php similarity index 93% rename from src/Router/RouteCollection.php rename to src/RouteCollection.php index 92d6c9d..941fb5c 100644 --- a/src/Router/RouteCollection.php +++ b/src/RouteCollection.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Membrane\OpenAPIRouter\Router; +namespace Membrane\OpenAPIRouter; -use Membrane\OpenAPIRouter\Router\Route\Server; +use Membrane\OpenAPIRouter\Route\Server; class RouteCollection { @@ -46,7 +46,10 @@ public function __construct( public static function fromServers(Server ...$servers): self { $filteredServers = array_filter($servers, fn($s) => !$s->isEmpty()); - usort($filteredServers, fn($a, $b) => $a->howManyDynamicComponents() <=> $b->howManyDynamicComponents()); + usort( + $filteredServers, + fn(Server $a, Server $b) => $a->howManyDynamicComponents() <=> $b->howManyDynamicComponents() + ); $hostedServers = $hostlessServers = []; foreach ($filteredServers as $server) { diff --git a/src/Router/Collector/RouteCollector.php b/src/RouteCollector.php similarity index 72% rename from src/Router/Collector/RouteCollector.php rename to src/RouteCollector.php index 45256a2..e8f7220 100644 --- a/src/Router/Collector/RouteCollector.php +++ b/src/RouteCollector.php @@ -2,16 +2,14 @@ declare(strict_types=1); -namespace Membrane\OpenAPIRouter\Router\Collector; +namespace Membrane\OpenAPIRouter; use cebe\openapi\spec\OpenApi; use cebe\openapi\spec\Operation; use cebe\openapi\spec\PathItem; -use Membrane\OpenAPIRouter\Exception\CannotProcessOpenAPI; -use Membrane\OpenAPIRouter\Exception\CannotRouteOpenAPI; -use Membrane\OpenAPIRouter\Router\Route\Route; -use Membrane\OpenAPIRouter\Router\Route\Server as ServerRoute; -use Membrane\OpenAPIRouter\Router\RouteCollection; +use Membrane\OpenAPIRouter\Exception\CannotCollectRoutes; +use Membrane\OpenAPIRouter\Route\Route; +use Membrane\OpenAPIRouter\Route\Server as ServerRoute; class RouteCollector { @@ -20,27 +18,12 @@ public function collect(OpenApi $openApi): RouteCollection $collection = $this->collectRoutes($openApi); if ($collection === []) { - throw CannotRouteOpenAPI::noRoutes(); + throw CannotCollectRoutes::noRoutes(); } return RouteCollection::fromServers(...$collection); } - /** @return array */ - 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)); - } - - private function getRegex(string $path): string - { - $regex = preg_replace('#{[^/]+}#', '([^/]+)', $path); - assert($regex !== null); // The pattern is hardcoded, valid regex so should not cause an error in preg_replace - - return $regex; - } - /** @return array */ private function collectRoutes(OpenApi $openApi): array { @@ -66,19 +49,6 @@ private function collectRoutes(OpenApi $openApi): array $collection[$url] ??= new ServerRoute($url, $regex); } - // TODO remove this conditional once OpenAPIFileReader requires operationId - if ($operationObject->operationId === null) { - throw CannotProcessOpenAPI::missingOperationId($path, $method); - } - - if (isset($operationIds[$operationObject->operationId])) { - throw CannotProcessOpenAPI::duplicateOperationId( - $operationObject->operationId, - $operationIds[$operationObject->operationId], - ['path' => $path, 'operation' => $method] - ); - } - if ($operationServers !== []) { $servers = $operationServers; } elseif ($pathServers !== []) { @@ -97,4 +67,19 @@ private function collectRoutes(OpenApi $openApi): array return array_filter($collection, fn($s) => !$s->isEmpty()); } + + /** @return array */ + 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)); + } + + private function getRegex(string $path): string + { + $regex = preg_replace('#{[^/]+}#', '([^/]+)', $path); + assert($regex !== null); // The pattern is hardcoded, valid regex so should not cause an error in preg_replace + + return $regex; + } } diff --git a/src/Router/Router.php b/src/Router.php similarity index 99% rename from src/Router/Router.php rename to src/Router.php index eec3b0f..e3c0289 100644 --- a/src/Router/Router.php +++ b/src/Router.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Membrane\OpenAPIRouter\Router; +namespace Membrane\OpenAPIRouter; use Membrane\OpenAPIRouter\Exception\CannotRouteRequest; diff --git a/tests/Console/Commands/CacheOpenAPIRoutesTest.php b/tests/Console/Command/CacheOpenAPIRoutesTest.php similarity index 86% rename from tests/Console/Commands/CacheOpenAPIRoutesTest.php rename to tests/Console/Command/CacheOpenAPIRoutesTest.php index 1222e12..5b0a486 100644 --- a/tests/Console/Commands/CacheOpenAPIRoutesTest.php +++ b/tests/Console/Command/CacheOpenAPIRoutesTest.php @@ -2,16 +2,15 @@ declare(strict_types=1); -namespace Membrane\OpenAPIRouter\Console\Commands; +namespace Membrane\OpenAPIRouter\Tests\Console\Command; -use Membrane\OpenAPIRouter\Exception\CannotReadOpenAPI; -use Membrane\OpenAPIRouter\Exception\CannotRouteOpenAPI; -use Membrane\OpenAPIRouter\Reader\OpenAPIFileReader; -use Membrane\OpenAPIRouter\Router\Collector\RouteCollector; -use Membrane\OpenAPIRouter\Router\Route\Route; -use Membrane\OpenAPIRouter\Router\RouteCollection; +use Membrane\OpenAPIRouter\Console\Command\CacheOpenAPIRoutes; +use Membrane\OpenAPIRouter\Console\Service; +use Membrane\OpenAPIRouter\Exception; +use Membrane\OpenAPIRouter\Route; +use Membrane\OpenAPIRouter\RouteCollection; +use Membrane\OpenAPIRouter\RouteCollector; use org\bovigo\vfs\vfsStream; -use org\bovigo\vfs\vfsStreamDirectory; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; @@ -21,20 +20,18 @@ use Symfony\Component\Console\Tester\CommandTester; #[CoversClass(CacheOpenAPIRoutes::class)] -#[CoversClass(CannotReadOpenAPI::class)] -#[CoversClass(CannotRouteOpenAPI::class)] -#[UsesClass(\Membrane\OpenAPIRouter\Console\Service\CacheOpenAPIRoutes::class)] -#[UsesClass(OpenAPIFileReader::class)] -#[UsesClass(Route::class)] +#[CoversClass(Exception\CannotCollectRoutes::class)] +#[UsesClass(Service\CacheOpenAPIRoutes::class)] +#[UsesClass(Route\Route::class), UsesClass(Route\Server::class), UsesClass(Route\Path::class)] #[UsesClass(RouteCollection::class)] #[UsesClass(RouteCollector::class)] class CacheOpenAPIRoutesTest extends TestCase { - private vfsStreamDirectory $root; + private string $root; public function setUp(): void { - $this->root = vfsStream::setup('cache'); + $this->root = vfsStream::setup('cache')->url(); } #[Test] @@ -55,31 +52,40 @@ public function outputsErrorForReadonlyFilePaths(): void ); } - public static function failedExecutionProvider(): array + #[Test] + public function itCannotRouteFromRelativeFilePaths(): void { - return [ - 'cannot read from relative filename' => [ - '/../../fixtures/docs/petstore-expanded.json', - vfsStream::url('cache') . '/routes.php', - Command::FAILURE, - ], - 'cannot route from an api with no routes' => [ - __DIR__ . '/../../fixtures/simple.json', - vfsStream::url('cache') . '/routes.php', - Command::FAILURE, - ], - ]; + $filePath = './tests/fixtures/docs/petstore-expanded.json'; + + self::assertTrue(file_exists($filePath)); + + $sut = new CommandTester(new CacheOpenAPIRoutes()); + + $sut->execute(['openAPI' => $filePath, 'destination' => vfsStream::url('cache') . '/routes.php']); + + self::assertSame(Command::FAILURE, $sut->getStatusCode()); } #[Test] - #[DataProvider('failedExecutionProvider')] - public function executeTest(string $openAPI, string $destination, int $expectedStatusCode): void + public function itCannotRouteWithoutAnyRoutes(): void { + $openAPIFilePath = $this->root . '/openapi.json'; + file_put_contents( + $openAPIFilePath, + json_encode([ + 'openapi' => '3.0.0', + 'info' => ['title' => '', 'version' => '1.0.0'], + 'paths' => [] + ]) + ); + + self::assertTrue(file_exists($openAPIFilePath)); + $sut = new CommandTester(new CacheOpenAPIRoutes()); - $sut->execute(['openAPI' => $openAPI, 'destination' => $destination]); + $sut->execute(['openAPI' => $openAPIFilePath, 'destination' => vfsStream::url('cache') . '/routes.php']); - self::assertSame($expectedStatusCode, $sut->getStatusCode()); + self::assertSame(Command::FAILURE, $sut->getStatusCode()); } public static function successfulExecutionProvider(): array diff --git a/tests/Console/Service/CacheOpenAPIRoutesTest.php b/tests/Console/Service/CacheOpenAPIRoutesTest.php index 2b36a49..b32c0c9 100644 --- a/tests/Console/Service/CacheOpenAPIRoutesTest.php +++ b/tests/Console/Service/CacheOpenAPIRoutesTest.php @@ -2,76 +2,68 @@ declare(strict_types=1); -namespace Console\Service; +namespace Membrane\OpenAPIRouter\Tests\Console\Service; use Membrane\OpenAPIRouter\Console\Service\CacheOpenAPIRoutes; -use Membrane\OpenAPIRouter\Exception\CannotReadOpenAPI; -use Membrane\OpenAPIRouter\Exception\CannotRouteOpenAPI; -use Membrane\OpenAPIRouter\Reader\OpenAPIFileReader; -use Membrane\OpenAPIRouter\Router\Collector\RouteCollector; -use Membrane\OpenAPIRouter\Router\Route\Route; -use Membrane\OpenAPIRouter\Router\RouteCollection; +use Membrane\OpenAPIRouter\Exception\CannotCollectRoutes; +use Membrane\OpenAPIRouter\Route; +use Membrane\OpenAPIRouter\RouteCollection; +use Membrane\OpenAPIRouter\RouteCollector; use org\bovigo\vfs\vfsStream; -use org\bovigo\vfs\vfsStreamDirectory; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; -use Symfony\Component\Console\Command\Command; #[CoversClass(CacheOpenAPIRoutes::class)] -#[CoversClass(CannotReadOpenAPI::class)] -#[CoversClass(CannotRouteOpenAPI::class)] -#[UsesClass(OpenAPIFileReader::class)] +#[CoversClass(CannotCollectRoutes::class)] #[UsesClass(RouteCollector::class)] -#[UsesClass(Route::class)] +#[UsesClass(Route\Route::class), UsesClass(Route\Server::class), UsesClass(Route\Path::class)] #[UsesClass(RouteCollection::class)] class CacheOpenAPIRoutesTest extends TestCase { - private vfsStreamDirectory $root; + private string $root; private CacheOpenAPIRoutes $sut; + private string $fixtures = __DIR__ . '/../../fixtures/'; public function setUp(): void { - $this->root = vfsStream::setup('cache'); + $this->root = vfsStream::setup()->url(); $this->sut = new CacheOpenAPIRoutes(self::createStub(LoggerInterface::class)); } #[Test] public function outputsErrorForReadonlyFilePaths(): void { - $readonlyDestination = $this->root->url('cache'); - chmod($readonlyDestination, 0444); + $cache = $this->root . '/cache'; + mkdir($cache); + chmod($cache, 0444); - self::assertFalse($this->sut->cache( - __DIR__ . '/../../fixtures/docs/petstore-expanded.json', - $readonlyDestination - )); + self::assertFalse($this->sut->cache($this->fixtures . 'docs/petstore-expanded.json', $cache)); } - public static function failedExecutionProvider(): array + #[Test] + public function cannotRouteWithoutPaths(): void { - return [ - 'cannot read from relative filename' => [ - '/../../fixtures/docs/petstore-expanded.json', - vfsStream::url('cache') . '/routes.php', - Command::FAILURE, - ], - 'cannot route from an api with no routes' => [ - __DIR__ . '/../../fixtures/simple.json', - vfsStream::url('cache') . '/routes.php', - Command::FAILURE, - ], - ]; + $openAPIFilePath = $this->root . '/openapi.json'; + file_put_contents( + $openAPIFilePath, + json_encode(['openapi' => '3.0.0', 'info' => ['title' => '', 'version' => '1.0.0'], 'paths' => []]) + ); + + self::assertFalse($this->sut->cache($openAPIFilePath, $this->root . '/cache/routes.php')); } #[Test] - #[DataProvider('failedExecutionProvider')] - public function executeTest(string $openAPI, string $destination): void + public function cannotRouteFromRelativeFilePaths(): void { - self::assertFalse($this->sut->cache($openAPI, $destination)); + $filePath = './tests/fixtures/docs/petstore-expanded.json'; + + self::assertTrue(file_exists($filePath)); + + self::assertFalse($this->sut->cache($filePath, $this->root . '/cache/routes.php')); } public static function successfulExecutionProvider(): array @@ -263,17 +255,17 @@ public static function successfulExecutionProvider(): array return [ 'successfully routes petstore-expanded.json' => [ __DIR__ . '/../../fixtures/docs/petstore-expanded.json', - vfsStream::url('cache/routes.php'), + vfsStream::url('root/cache/routes.php'), $petStoreRoutes ], 'successfully routes the WeirdAndWonderful.json' => [ __DIR__ . '/../../fixtures/WeirdAndWonderful.json', - vfsStream::url('cache/routes.php'), + vfsStream::url('root/cache/routes.php'), $weirdAndWonderfulRoutes ], 'successfully routes the WeirdAndWonderful.json and caches in a nested directory' => [ __DIR__ . '/../../fixtures/WeirdAndWonderful.json', - vfsStream::url('cache/nested-cache/nester-cache/nestest-cache/routes.php'), + vfsStream::url('root/cache/nested-cache/nester-cache/nestest-cache/routes.php'), $weirdAndWonderfulRoutes ] ]; diff --git a/tests/Reader/OpenAPIFileReaderTest.php b/tests/Reader/OpenAPIFileReaderTest.php deleted file mode 100644 index e59b6b7..0000000 --- a/tests/Reader/OpenAPIFileReaderTest.php +++ /dev/null @@ -1,188 +0,0 @@ -vfsRoot = vfsStream::setup(); - } - - #[Test] - public function readerThrowsExceptionIfFileNotFound(): void - { - $filePath = $this->vfsRoot->url() . '/openapi.json'; - - self::expectExceptionObject(CannotReadOpenAPI::fileNotFound($filePath)); - - (new OpenAPIFileReader())->readFromAbsoluteFilePath($filePath); - } - - #[Test] - public function readerThrowsExceptionForUnsupportedFileTypes(): void - { - $structure = ['openapi.txt' => 'some text']; - vfsStream::create($structure); - $filePath = $this->vfsRoot->url() . '/openapi.txt'; - - self::expectExceptionObject(CannotReadOpenAPI::fileTypeNotSupported(pathinfo($filePath, PATHINFO_EXTENSION))); - - (new OpenAPIFileReader())->readFromAbsoluteFilePath($filePath); - } - - #[Test] - public function readerThrowsExceptionIfJsonCannotBeParsedAsOpenAPI(): void - { - $structure = [ - 'openapi.json' => - <<vfsRoot->url() . '/openapi.json'; - - self::expectExceptionObject(CannotReadOpenAPI::cannotParse('openapi.json', new TypeError())); - - (new OpenAPIFileReader())->readFromAbsoluteFilePath($filePath); - } - - #[Test] - public function readerThrowsExceptionIfYamlCannotBeParsedAsOpenAPI(): void - { - $structure = [ - 'openapi.yaml' => - <<vfsRoot->url() . '/openapi.yaml'; - - self::expectExceptionObject(CannotReadOpenAPI::cannotParse('openapi.yaml', new ParseException(''))); - - (new OpenAPIFileReader())->readFromAbsoluteFilePath($filePath); - } - - #[Test] - public function readerThrowsExceptionIfYamlContainsInvalidOpenAPI(): void - { - $structure = [ - 'openapi.yaml' => - <<vfsRoot->url() . '/openapi.yaml'; - - self::expectExceptionObject(CannotReadOpenAPI::invalidOpenAPI('openapi.yaml')); - - (new OpenAPIFileReader())->readFromAbsoluteFilePath($filePath); - } - - #[Test] - public function readerThrowsExceptionIfJsonContainsInvalidOpenAPI(): void - { - $structure = [ - 'openapi.json' => - <<vfsRoot->url() . '/openapi.json'; - - self::expectExceptionObject(CannotReadOpenAPI::invalidOpenAPI('openapi.json')); - - (new OpenAPIFileReader())->readFromAbsoluteFilePath($filePath); - } - - #[Test] - public function throwsExceptionForRelativeFilePaths(): void - { - self::expectExceptionObject( - CannotReadOpenAPI::unresolvedReference('petstore.yaml', new UnresolvableReferenceException()) - ); - - (new OpenAPIFileReader())->readFromAbsoluteFilePath('./tests/fixtures/docs/petstore.yaml'); - } - - - #[Test] - public function returnsOpenAPIObjectFromJsonWithValidOpenAPI(): void - { - $structure = [ - 'openapi.json' => - <<vfsRoot->url() . '/openapi.json'; - - $actual = (new OpenAPIFileReader())->readFromAbsoluteFilePath($filePath); - - self::assertInstanceOf(OpenApi::class, $actual); - } - - #[Test] - public function returnsOpenAPIObjectFromYamlWithValidOpenAPI(): void - { - $structure = [ - 'openapi.yaml' => - <<vfsRoot->url() . '/openapi.yaml'; - - $actual = (new OpenAPIFileReader())->readFromAbsoluteFilePath($filePath); - - self::assertInstanceOf(OpenApi::class, $actual); - } -} diff --git a/tests/RouteCollectorTest.php b/tests/RouteCollectorTest.php new file mode 100644 index 0000000..3418059 --- /dev/null +++ b/tests/RouteCollectorTest.php @@ -0,0 +1,280 @@ +readFromString( + json_encode([ + 'openapi' => '3.0.0', + 'info' => ['title' => '', 'version' => '1.0.0'], + 'paths' => [] + ]), + FileFormat::Json + ); + + self::expectExceptionObject(CannotCollectRoutes::noRoutes()); + + (new RouteCollector())->collect($openAPI); + } + + public static function collectTestProvider(): Generator + { + yield 'petstore-expanded.json' => [ + new RouteCollection([ + 'hosted' => [ + 'static' => [ + 'http://petstore.swagger.io/api' => [ + 'static' => [ + '/pets' => [ + 'get' => 'findPets', + 'post' => 'addPet', + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|/pets/([^/]+)(*MARK:/pets/{id}))$#', + 'paths' => [ + '/pets/{id}' => [ + 'get' => 'find pet by id', + 'delete' => 'deletePet', + ], + ], + ], + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|)#', + 'servers' => [], + ], + ], + 'hostless' => [ + 'static' => [], + 'dynamic' => [ + 'regex' => '#^(?|)#', + 'servers' => [], + ], + ], + ]), + self::FIXTURES . 'docs/petstore-expanded.json', + ]; + + yield 'WeirdAndWonderful.json' => [ + new RouteCollection([ + 'hosted' => [ + 'static' => [ + 'http://weirdest.com' => [ + 'static' => [ + '/however' => [ + 'put' => 'put-however', + 'post' => 'post-however', + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|/and/([^/]+)(*MARK:/and/{name}))$#', + 'paths' => [ + '/and/{name}' => [ + 'get' => 'get-and', + ], + ], + ], + ], + 'http://weirder.co.uk' => [ + 'static' => [ + '/however' => [ + 'get' => 'get-however', + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|/and/([^/]+)(*MARK:/and/{name}))$#', + 'paths' => [ + '/and/{name}' => [ + 'put' => 'put-and', + 'post' => 'post-and', + ], + ], + ], + ], + 'http://wonderful.io' => [ + 'static' => [ + '/or' => [ + 'post' => 'post-or', + ], + '/xor' => [ + 'delete' => 'delete-xor', + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|)$#', + 'paths' => [], + ], + ], + 'http://wonderful.io/and' => [ + 'static' => [ + '/or' => [ + 'post' => 'post-or', + ], + '/xor' => [ + 'delete' => 'delete-xor', + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|)$#', + 'paths' => [], + ], + ], + 'http://wonderful.io/or' => [ + 'static' => [ + '/or' => [ + 'post' => 'post-or', + ], + '/xor' => [ + 'delete' => 'delete-xor', + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|)$#', + 'paths' => [], + ], + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|http://weird.io/([^/]+)(*MARK:http://weird.io/{conjunction}))#', + 'servers' => [ + 'http://weird.io/{conjunction}' => [ + 'static' => [ + '/or' => [ + 'post' => 'post-or', + ], + '/xor' => [ + 'delete' => 'delete-xor', + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|)$#', + 'paths' => [], + ], + ], + ], + ], + ], + 'hostless' => [ + 'static' => [ + '' => [ + 'static' => [ + '/or' => [ + 'post' => 'post-or', + ], + '/xor' => [ + 'delete' => 'delete-xor', + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|)$#', + 'paths' => [], + ], + ], + '/v1' => [ + 'static' => [ + '/or' => [ + 'post' => 'post-or', + ], + '/xor' => [ + 'delete' => 'delete-xor', + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|)$#', + 'paths' => [], + ], + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|/([^/]+)(*MARK:/{version}))#', + 'servers' => [ + '/{version}' => [ + 'static' => [ + '/or' => [ + 'post' => 'post-or', + ], + '/xor' => [ + 'delete' => 'delete-xor', + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|)$#', + 'paths' => [], + ], + ], + ], + ], + ], + ]), + self::FIXTURES . 'WeirdAndWonderful.json', + ]; + yield 'APieceOfCake.json' => [ + new RouteCollection([ + 'hosted' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], + 'hostless' => [ + 'static' => [ + '' => [ + 'static' => ['/cakes/sponge' => ['get' => 'findSpongeCakes']], + 'dynamic' => [ + 'regex' => '#^(?|' . + '/cakes/([^/]+)(*MARK:/cakes/{icing})|' . + '/([^/]+)/sponge(*MARK:/{cakeType}/sponge)|' . + '/([^/]+)/([^/]+)(*MARK:/{cakeType}/{icing})' . + ')$#', + 'paths' => [ + '/cakes/{icing}' => ['get' => 'findCakesByIcing', 'post' => 'addCakesByIcing'], + '/{cakeType}/sponge' => ['get' => 'findSpongeByDesserts'], + '/{cakeType}/{icing}' => [ + 'get' => 'findDessertByIcing', + 'post' => 'addDessertByIcing' + ] + ] + ] + ], + ], + 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []] + ], + ]), + self::FIXTURES . 'APIeceOfCake.json' + ]; + } + + #[Test] + #[DataProvider('collectTestProvider')] + public function collectTest(RouteCollection $expected, string $apiFilePath): void + { + $openAPI = (new Reader([OpenAPIVersion::Version_3_0, OpenAPIVersion::Version_3_1])) + ->readFromAbsoluteFilePath($apiFilePath); + + self::assertEquals($expected, (new RouteCollector())->collect($openAPI)); + } +} diff --git a/tests/Router/APIeceOfCakeTest.php b/tests/Router/APIeceOfCakeTest.php deleted file mode 100644 index 4c44762..0000000 --- a/tests/Router/APIeceOfCakeTest.php +++ /dev/null @@ -1,78 +0,0 @@ -readFromAbsoluteFilePath($apiFilePath); - $routeCollection = (new RouteCollector())->collect($api); - - // I expect to have no hosted routes - self::assertEmpty($routeCollection->routes['hosted']['static']); - self::assertEmpty($routeCollection->routes['hosted']['dynamic']['servers']); - - // I expect to have one hostless static route - self::assertSame( - 'findSpongeCakes', - $routeCollection->routes['hostless']['static']['']['static']['/cakes/sponge']['get'] - ); - - // I expect to have the following hostless dynamic routes - $hostlessDynamicRoutes = [ - '/cakes/{icing}' => ['get' => 'findCakesByIcing', 'post' => 'addCakesByIcing'], - '/{cakeType}/sponge' => ['get' => 'findSpongeByDesserts'], - '/{cakeType}/{icing}' => ['get' => 'findDessertByIcing', 'post' => 'addDessertByIcing'], - - ]; - self::assertSame( - $hostlessDynamicRoutes, - $routeCollection->routes['hostless']['static']['']['dynamic']['paths'] - ); - - return $routeCollection; - } - - #[Test, TestDox('Completely static paths should take priority over any other')] - #[Depends('itCollectsPathsFromAPIeceOfCake')] - public function itRoutesToCompletelyStaticPathFirst(RouteCollection $routeCollection): void - { - $expectedOperationId = 'findSpongeCakes'; - $sut = new Router($routeCollection); - - $actualOperationId = $sut->route('/cakes/sponge', 'get'); - - self::assertSame($expectedOperationId, $actualOperationId); - } - - #[Test, TestDox('Paths with less dynamic elements should take priority')] - #[Depends('itCollectsPathsFromAPIeceOfCake')] - public function itRoutesToPartiallyDynamicBeforeCompletelyDynamic(RouteCollection $routeCollection): void - { - $expectedOperationId = 'findCakesByIcing'; - $sut = new Router($routeCollection); - - $actualOperationId = $sut->route('/cakes/chocolate', 'get'); - - self::assertSame($expectedOperationId, $actualOperationId); - } -} diff --git a/tests/Router/Collector/RouteCollectorTest.php b/tests/Router/Collector/RouteCollectorTest.php deleted file mode 100644 index c4072e3..0000000 --- a/tests/Router/Collector/RouteCollectorTest.php +++ /dev/null @@ -1,270 +0,0 @@ -collect($openApi); - } - - #[Test] - public function throwsExceptionForMissingOperationId(): void - { - $sut = new RouteCollector(); - $openApi = Reader::readFromYamlFile(self::FIXTURES . 'missingOperationId.yaml'); - - self::expectExceptionObject(CannotProcessOpenAPI::missingOperationId('/path', 'get')); - - $sut->collect($openApi); - } - - #[Test] - public function throwsExceptionForDuplicateOperationId(): void - { - $sut = new RouteCollector(); - $openApi = Reader::readFromYamlFile(self::FIXTURES . 'duplicateOperationId.yaml'); - - self::expectExceptionObject(CannotProcessOpenAPI::duplicateOperationId( - 'operation1', - ['path' => '/path', 'operation' => 'get'], - ['path' => '/path', 'operation' => 'delete'], - )); - - $sut->collect($openApi); - } - - public static function collectTestProvider(): array - { - return [ - 'petstore-expanded.json' => [ - new RouteCollection([ - 'hosted' => [ - 'static' => [ - 'http://petstore.swagger.io/api' => [ - 'static' => [ - '/pets' => [ - 'get' => 'findPets', - 'post' => 'addPet', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|/pets/([^/]+)(*MARK:/pets/{id}))$#', - 'paths' => [ - '/pets/{id}' => [ - 'get' => 'find pet by id', - 'delete' => 'deletePet', - ], - ], - ], - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)#', - 'servers' => [], - ], - ], - 'hostless' => [ - 'static' => [], - 'dynamic' => [ - 'regex' => '#^(?|)#', - 'servers' => [], - ], - ], - ]), - self::FIXTURES . 'docs/petstore-expanded.json', - ], - 'WeirdAndWonderful.json' => [ - new RouteCollection([ - 'hosted' => [ - 'static' => [ - 'http://weirdest.com' => [ - 'static' => [ - '/however' => [ - 'put' => 'put-however', - 'post' => 'post-however', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|/and/([^/]+)(*MARK:/and/{name}))$#', - 'paths' => [ - '/and/{name}' => [ - 'get' => 'get-and', - ], - ], - ], - ], - 'http://weirder.co.uk' => [ - 'static' => [ - '/however' => [ - 'get' => 'get-however', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|/and/([^/]+)(*MARK:/and/{name}))$#', - 'paths' => [ - '/and/{name}' => [ - 'put' => 'put-and', - 'post' => 'post-and', - ], - ], - ], - ], - 'http://wonderful.io' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - 'http://wonderful.io/and' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - 'http://wonderful.io/or' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|http://weird.io/([^/]+)(*MARK:http://weird.io/{conjunction}))#', - 'servers' => [ - 'http://weird.io/{conjunction}' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - ], - ], - ], - 'hostless' => [ - 'static' => [ - '' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - '/v1' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|/([^/]+)(*MARK:/{version}))#', - 'servers' => [ - '/{version}' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - ], - ], - ], - ]), - self::FIXTURES . 'WeirdAndWonderful.json', - ], - ]; - } - - #[Test] - #[DataProvider('collectTestProvider')] - public function collectTest(RouteCollection $expected, string $apiFilePath): void - { - $openApi = Reader::readFromJsonFile($apiFilePath); - $sut = new RouteCollector(); - - $actual = $sut->collect($openApi); - - self::assertEquals($expected, $actual); - } -} diff --git a/tests/Router/RouterTest.php b/tests/Router/RouterTest.php deleted file mode 100644 index 8edcaff..0000000 --- a/tests/Router/RouterTest.php +++ /dev/null @@ -1,316 +0,0 @@ - [ - 'static' => [ - 'http://petstore.swagger.io/api' => [ - 'static' => [ - '/pets' => [ - 'get' => 'findPets', - 'post' => 'addPet', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|/pets/([^/]+)(*MARK:/pets/{id}))$#', - 'paths' => [ - '/pets/{id}' => [ - 'get' => 'find pet by id', - 'delete' => 'deletePet', - ], - ], - ], - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)#', - 'servers' => [], - ], - ], - 'hostless' => [ - 'static' => [], - 'dynamic' => [ - 'regex' => '#^(?|)#', - 'servers' => [], - ], - ], - ]); - } - - private static function getWeirdAndWonderfulRouteCollection(): RouteCollection - { - return new RouteCollection([ - 'hosted' => [ - 'static' => [ - 'http://weirdest.com' => [ - 'static' => [ - '/however' => [ - 'put' => 'put-however', - 'post' => 'post-however', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|/and/([^/]+)(*MARK:/and/{name}))$#', - 'paths' => [ - '/and/{name}' => [ - 'get' => 'get-and', - ], - ], - ], - ], - 'http://weirder.co.uk' => [ - 'static' => [ - '/however' => [ - 'get' => 'get-however', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|/and/([^/]+)(*MARK:/and/{name}))$#', - 'paths' => [ - '/and/{name}' => [ - 'put' => 'put-and', - 'post' => 'post-and', - ], - ], - ], - ], - 'http://wonderful.io' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - 'http://wonderful.io/and' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - 'http://wonderful.io/or' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|http://weird.io/([^/]+)(*MARK:http://weird.io/{conjunction}))#', - 'servers' => [ - 'http://weird.io/{conjunction}' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - ], - ], - ], - 'hostless' => [ - 'static' => [ - '' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - '/v1' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|/([^/]+)(*MARK:/{version}))#', - 'servers' => [ - '/{version}' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|)$#', - 'paths' => [], - ], - ], - ], - ], - ], - ]); - } - - public static function unsuccessfulRouteProvider(): array - { - return [ - 'petstore-expanded: incorrect server url' => [ - CannotRouteRequest::notFound(), - 'https://hatshop.dapper.net/api/pets', - 'get', - self::getPetStoreRouteCollection(), - ], - 'petstore-expanded: correct static server url but incorrect path' => [ - CannotRouteRequest::notFound(), - 'http://petstore.swagger.io/api/hats', - 'get', - self::getPetStoreRouteCollection(), - ], - 'WeirdAndWonderful: correct dynamic erver url but incorrect path' => [ - CannotRouteRequest::notFound(), - 'http://weird.io/however/but', - 'get', - self::getWeirdAndWonderfulRouteCollection(), - ], - 'petstore-expanded: correct url but incorrect method' => [ - CannotRouteRequest::methodNotAllowed(), - 'http://petstore.swagger.io/api/pets', - 'delete', - self::getPetStoreRouteCollection(), - ], - ]; - } - - #[Test] - #[DataProvider('unsuccessfulRouteProvider')] - public function unsuccessfulRouteTest( - CannotRouteRequest $expected, - string $path, - string $method, - RouteCollection $operationCollection - ): void { - $sut = new Router($operationCollection); - - self::expectExceptionObject($expected); - - $sut->route($path, $method); - } - - public static function successfulRouteProvider(): array - { - return [ - 'petstore: /pets path, get method' => [ - 'findPets', - 'http://petstore.swagger.io/api/pets', - 'get', - self::getPetStoreRouteCollection(), - ], - 'petstore: /pets/{id} path, get method' => [ - 'find pet by id', - 'http://petstore.swagger.io/api/pets/1', - 'get', - self::getPetStoreRouteCollection(), - ], - 'petstore: /pets/{id} path, delete method' => [ - 'deletePet', - 'http://petstore.swagger.io/api/pets/1', - 'delete', - self::getPetStoreRouteCollection(), - ], - 'WeirdAndWonderful: /v1/or path, post method' => [ - 'post-or', - '/v1/or', - 'post', - self::getWeirdAndWonderfulRouteCollection(), - ], - 'WeirdAndWonderful: http://www.arbitrary.com/v1/or path, post method' => [ - 'post-or', - '/v1/or', - 'post', - self::getWeirdAndWonderfulRouteCollection(), - ], - 'WeirdAndWonderful: http://weird.io/however/or path, post method' => [ - 'post-or', - 'http://weird.io/however/or', - 'post', - self::getWeirdAndWonderfulRouteCollection(), - ], - 'WeirdAndWonderful: /{version}/xor path, delete method' => [ - 'delete-xor', - '/12/xor', - 'delete', - self::getWeirdAndWonderfulRouteCollection(), - ], - ]; - } - - #[Test] - #[DataProvider('successfulRouteProvider')] - public function successfulRouteTest( - string $expected, - string $path, - string $method, - RouteCollection $operationCollection - ): void { - $sut = new Router($operationCollection); - - $actual = $sut->route($path, $method); - - self::assertSame($expected, $actual); - } -} diff --git a/tests/RouterTest.php b/tests/RouterTest.php new file mode 100644 index 0000000..b8abd2c --- /dev/null +++ b/tests/RouterTest.php @@ -0,0 +1,188 @@ +readFromAbsoluteFilePath(self::FIXTURES . 'docs/petstore-expanded.json'); + + return (new RouteCollector())->collect($openAPI); + } + + private static function getWeirdAndWonderfulRouteCollection(): RouteCollection + { + $openAPI = (new Reader([OpenAPIVersion::Version_3_0])) + ->readFromAbsoluteFilePath(self::FIXTURES . 'WeirdAndWonderful.json'); + + return (new RouteCollector())->collect($openAPI); + } + + private static function getAPieceOfCakeRouteCollection(): RouteCollection + { + $openAPI = (new Reader([OpenAPIVersion::Version_3_0])) + ->readFromAbsoluteFilePath(self::FIXTURES . 'APIeceOfCake.json'); + + return (new RouteCollector())->collect($openAPI); + } + + public static function unsuccessfulRouteProvider(): array + { + return [ + 'petstore-expanded: incorrect server url' => [ + CannotRouteRequest::notFound(), + 'https://hatshop.dapper.net/api/pets', + 'get', + self::getPetStoreRouteCollection(), + ], + 'petstore-expanded: correct static server url but incorrect path' => [ + CannotRouteRequest::notFound(), + 'http://petstore.swagger.io/api/hats', + 'get', + self::getPetStoreRouteCollection(), + ], + 'WeirdAndWonderful: correct dynamic erver url but incorrect path' => [ + CannotRouteRequest::notFound(), + 'http://weird.io/however/but', + 'get', + self::getWeirdAndWonderfulRouteCollection(), + ], + 'petstore-expanded: correct url but incorrect method' => [ + CannotRouteRequest::methodNotAllowed(), + 'http://petstore.swagger.io/api/pets', + 'delete', + self::getPetStoreRouteCollection(), + ], + ]; + } + + #[Test] + #[DataProvider('unsuccessfulRouteProvider')] + public function unsuccessfulRouteTest( + CannotRouteRequest $expected, + string $path, + string $method, + RouteCollection $operationCollection + ): void { + $sut = new Router($operationCollection); + + self::expectExceptionObject($expected); + + $sut->route($path, $method); + } + + public static function successfulRouteProvider(): array + { + return [ + 'petstore: /pets path, get method' => [ + 'findPets', + 'http://petstore.swagger.io/api/pets', + 'get', + self::getPetStoreRouteCollection(), + ], + 'petstore: /pets/{id} path, get method' => [ + 'find pet by id', + 'http://petstore.swagger.io/api/pets/1', + 'get', + self::getPetStoreRouteCollection(), + ], + 'petstore: /pets/{id} path, delete method' => [ + 'deletePet', + 'http://petstore.swagger.io/api/pets/1', + 'delete', + self::getPetStoreRouteCollection(), + ], + 'WeirdAndWonderful: /v1/or path, post method' => [ + 'post-or', + '/v1/or', + 'post', + self::getWeirdAndWonderfulRouteCollection(), + ], + 'WeirdAndWonderful: http://www.arbitrary.com/v1/or path, post method' => [ + 'post-or', + '/v1/or', + 'post', + self::getWeirdAndWonderfulRouteCollection(), + ], + 'WeirdAndWonderful: http://weird.io/however/or path, post method' => [ + 'post-or', + 'http://weird.io/however/or', + 'post', + self::getWeirdAndWonderfulRouteCollection(), + ], + 'WeirdAndWonderful: /{version}/xor path, delete method' => [ + 'delete-xor', + '/12/xor', + 'delete', + self::getWeirdAndWonderfulRouteCollection(), + ], + ]; + } + + #[Test] + #[DataProvider('successfulRouteProvider')] + public function successfulRouteTest( + string $expected, + string $path, + string $method, + RouteCollection $operationCollection + ): void { + $sut = new Router($operationCollection); + + $actual = $sut->route($path, $method); + + self::assertSame($expected, $actual); + } + + public static function provideRoutingPriorities(): Generator + { + yield 'completely static path prioritise over anything dynamic' => [ + 'findSpongeCakes', + '/cakes/sponge', + 'get', + self::getAPieceOfCakeRouteCollection() + ]; + yield 'partially dynamic path to prioritise over anything with more dynamic parts' => [ + 'findCakesByIcing', + '/cakes/chocolate', + 'get', + self::getAPieceOfCakeRouteCollection() + ]; + } + + #[Test, TestDox('When routing the priority will be paths with less dynamic components first')] + #[DataProvider('provideRoutingPriorities')] + public function itWillPrioritiseRoutesWithMoreStaticComponentsFirst( + string $expectedOperationId, + string $url, + string $method, + RouteCollection $routeCollection + ): void { + $sut = new Router($routeCollection); + + $actualOperationId = $sut->route($url, $method); + + self::assertSame($expectedOperationId, $actualOperationId); + } +} diff --git a/tests/fixtures/docs/petstore.yaml b/tests/fixtures/docs/petstore.yaml deleted file mode 100644 index 5910232..0000000 --- a/tests/fixtures/docs/petstore.yaml +++ /dev/null @@ -1,113 +0,0 @@ -openapi: "3.0.0" -info: - version: 1.0.0 - title: Swagger Petstore - license: - name: MIT -servers: - - url: http://petstore.swagger.io/v1 -paths: - /pets: - get: - summary: List all pets - operationId: listPets - tags: - - pets - parameters: - - name: limit - in: query - description: How many items to return at one time (max 100) - required: false - schema: - type: integer - maximum: 100 - format: int32 - responses: - '200': - description: A paged array of pets - headers: - x-next: - description: A link to the next page of responses - schema: - type: string - content: - application/json: - schema: - $ref: "#/components/schemas/Pets" - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - post: - summary: Create a pet - operationId: createPets - tags: - - pets - responses: - '201': - description: Null response - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /pets/{petId}: - get: - summary: Info for a specific pet - operationId: showPetById - tags: - - pets - parameters: - - name: petId - in: path - required: true - description: The id of the pet to retrieve - schema: - type: string - responses: - '200': - description: Expected response to a valid request - content: - application/json: - schema: - $ref: "#/components/schemas/Pet" - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" -components: - schemas: - Pet: - type: object - required: - - id - - name - properties: - id: - type: integer - format: int64 - name: - type: string - tag: - type: string - Pets: - type: array - maxItems: 100 - items: - $ref: "#/components/schemas/Pet" - Error: - type: object - required: - - code - - message - properties: - code: - type: integer - format: int32 - message: - type: string diff --git a/tests/fixtures/duplicateOperationId.yaml b/tests/fixtures/duplicateOperationId.yaml deleted file mode 100644 index fbedc9a..0000000 --- a/tests/fixtures/duplicateOperationId.yaml +++ /dev/null @@ -1,39 +0,0 @@ ---- -openapi: 3.0.0 -info: - title: Test API - version: 1.0.0 -servers: - - url: http://test.com -paths: - "/path": - get: - operationId: operation1 - parameters: - - name: id - in: header - required: true - schema: - type: integer - responses: - '200': - description: Successful Response - content: - application/json: - schema: - type: integer - delete: - operationId: operation1 - parameters: - - name: id - in: header - required: true - schema: - type: integer - responses: - '204': - description: Successful Response - content: - application/json: - schema: - type: integer diff --git a/tests/fixtures/missingOperationId.yaml b/tests/fixtures/missingOperationId.yaml deleted file mode 100644 index 0ae712a..0000000 --- a/tests/fixtures/missingOperationId.yaml +++ /dev/null @@ -1,23 +0,0 @@ ---- -openapi: 3.0.0 -info: - title: Test API - version: 1.0.0 -servers: - - url: http://test.com -paths: - "/path": - get: - parameters: - - name: id - in: header - required: true - schema: - type: integer - responses: - '200': - description: Successful Response - content: - application/json: - schema: - type: integer diff --git a/tests/fixtures/noReferences.json b/tests/fixtures/noReferences.json deleted file mode 100644 index 58b6297..0000000 --- a/tests/fixtures/noReferences.json +++ /dev/null @@ -1,1646 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Test API", - "version": "1.0.0" - }, - "servers": [ - { - "url": "http://www.test.com" - }, - { - "url": "http://www.test.com/path", - "description": "this server contains the path name '/path' inside" - } - ], - "paths": { - "/emptypath": { - "description": "/emptypath has no operations" - }, - "/path": { - "description": "/path has all operations, only DELETE has a default response", - "get": { - "description": "Get operation on /path, no requestBody", - "responses": { - "200": { - "description": "successful response, no content" - } - } - }, - "post": { - "requestBody": { - "description": "Post operation on /path, requestBody->content is empty", - "required": true, - "content": {} - }, - "responses": { - "200": { - "description": "successful response with content that is empty", - "content": {} - } - } - }, - "put": { - "requestBody": { - "description": "Put operation on /path, requestBody content is not application/json", - "required": true, - "content": { - "application/pdf": { - "schema": { - "type": "integer" - } - } - } - }, - "responses": { - "200": { - "description": "successful response with content that is not application/json", - "content": { - "application/pdf": { - "schema": { - "type": "integer" - } - } - } - } - } - }, - "delete": { - "requestBody": { - "description": "Delete operation on /path, requestBody content is application/json", - "required": true, - "content": { - "application/json": { - "schema": { - "type": "integer" - } - } - } - }, - "responses": { - "200": { - "description": "successful response with content that is application/json", - "content": { - "application/json": { - "schema": { - "type": "integer" - } - } - } - }, - "default": { - "description": "default response with content that is application/json", - "content": { - "application/json": { - "schema": { - "type": "integer" - } - } - } - } - } - } - }, - "/parampath/{id}": { - "description": "/parampath has a parameter in the path, applying to all operations", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "get": { - "description": "Get operation in /parampath/{id}, should have two parameters, 'id' and 'name'", - "parameters": [ - { - "name": "name", - "in": "header", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "successful response" - } - } - } - }, - "/requestpathexceptions": { - "get": { - "parameters": [ - { - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "" - } - } - }, - "post": { - "parameters": [ - { - "name": "id", - "in": "query", - "content": { - "application/pdf": { - "schema": { - "type": "integer" - } - } - } - } - ], - "responses": { - "200": { - "description": "" - } - } - } - }, - "/requestpathone/{id}": { - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "get": { - "responses": { - "200": { - "description": "" - } - } - }, - "post": { - "parameters": [ - { - "name": "names", - "in": "query", - "schema": { - "type": "array" - } - } - ], - "responses": { - "200": { - "description": "" - } - } - }, - "put": { - "parameters": [ - { - "name": "name", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - } - }, - "delete": { - "parameters": [ - { - "name": "name", - "in": "query", - "required": true, - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - ], - "responses": { - "200": { - "description": "" - } - } - } - }, - "/requestpathtwo": { - "parameters": [ - { - "name": "id", - "in": "header", - "schema": { - "type": "integer" - } - } - ], - "get": { - "responses": { - "200": { - "description": "" - } - } - }, - "post": { - "parameters": [ - { - "name": "name", - "in": "cookie", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - } - }, - "put": { - "parameters": [ - { - "name": "id", - "in": "query", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "" - } - } - }, - "delete": { - "parameters": [ - { - "name": "id", - "in": "header", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - } - } - }, - "/requestbodypath": { - "get": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "integer" - } - } - } - }, - "responses": { - "200": { - "description": "" - } - } - }, - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "integer" - } - } - } - }, - "parameters": [ - { - "name": "id", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - } - } - }, - "/requestbodypath/{id}": { - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "get": { - "parameters": [ - { - "name": "name", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "species", - "in": "header", - "schema": { - "type": "string" - } - }, - { - "name": "subspecies", - "in": "cookie", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "number", - "format": "float" - } - } - } - }, - "responses": { - "200": { - "description": "" - } - } - } - }, - "/responsepath": { - "get": { - "responses": { - "200": { - "description": "int", - "content": { - "application/json": { - "schema": { - "type": "integer" - } - } - } - }, - "201": { - "description": "nullable int", - "content": { - "application/json": { - "schema": { - "type": "integer", - "nullable": true - } - } - } - }, - "202": { - "description": "int, inclusive min", - "content": { - "application/json": { - "schema": { - "type": "integer", - "minimum": 0 - } - } - } - }, - "203": { - "description": "int, exclusive min", - "content": { - "application/json": { - "schema": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true - } - } - } - }, - "204": { - "description": "int, inclusive max", - "content": { - "application/json": { - "schema": { - "type": "integer", - "maximum": 100 - } - } - } - }, - "205": { - "description": "int, exclusive max", - "content": { - "application/json": { - "schema": { - "type": "integer", - "maximum": 100, - "exclusiveMaximum": true - } - } - } - }, - "206": { - "description": "int, multipleOf", - "content": { - "application/json": { - "schema": { - "type": "integer", - "multipleOf": 3 - } - } - } - }, - "207": { - "description": "int, enum", - "content": { - "application/json": { - "schema": { - "type": "integer", - "enum": [ - 1, - 2, - 3 - ] - } - } - } - }, - "209": { - "description": "nullable int, exclusive min, inclusive max, multipleOf, enum", - "content": { - "application/json": { - "schema": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true, - "maximum": 100, - "multipleOf": 3, - "enum": [ - 1, - 2, - 3 - ], - "nullable": true - } - } - } - }, - "210": { - "description": "number", - "content": { - "application/json": { - "schema": { - "type": "number" - } - } - } - }, - "211": { - "description": "nullable number", - "content": { - "application/json": { - "schema": { - "type": "number", - "nullable": true - } - } - } - }, - "212": { - "description": "number, enum", - "content": { - "application/json": { - "schema": { - "type": "number", - "enum": [ - 1, - 2.3, - 4 - ] - } - } - } - }, - "213": { - "description": "number, float format", - "content": { - "application/json": { - "schema": { - "type": "number", - "format": "float" - } - } - } - }, - "214": { - "description": "nullable number, float format", - "content": { - "application/json": { - "schema": { - "type": "number", - "format": "float", - "nullable": true - } - } - } - }, - "215": { - "description": "number, double format", - "content": { - "application/json": { - "schema": { - "type": "number", - "format": "double" - } - } - } - }, - "219": { - "description": "nullable number, enum, inclusive min, exclusive max, multipleOf", - "content": { - "application/json": { - "schema": { - "type": "number", - "minimum": 6.66, - "maximum": 99.99, - "exclusiveMaximum": true, - "multipleOf": 3.33, - "enum": [ - 1, - 2.3, - 4 - ], - "nullable": true - } - } - } - }, - "220": { - "description": "string", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - }, - "221": { - "description": "nullable string", - "content": { - "application/json": { - "schema": { - "type": "string", - "nullable": true - } - } - } - }, - "222": { - "description": "string, enum", - "content": { - "application/json": { - "schema": { - "type": "string", - "enum": [ - "a", - "b", - "c" - ] - } - } - } - }, - "223": { - "description": "string, date format", - "content": { - "application/json": { - "schema": { - "type": "string", - "format": "date" - } - } - } - }, - "224": { - "description": "string, date-time format", - "content": { - "application/json": { - "schema": { - "type": "string", - "format": "date-time" - } - } - } - }, - "225": { - "description": "string, minLength", - "content": { - "application/json": { - "schema": { - "type": "string", - "minLength": 5 - } - } - } - }, - "226": { - "description": "string, maxLength", - "content": { - "application/json": { - "schema": { - "type": "string", - "maxLength": 10 - } - } - } - }, - "227": { - "description": "string, pattern", - "content": { - "application/json": { - "schema": { - "type": "string", - "pattern": "[A-Za-z]+" - } - } - } - }, - "229": { - "description": "nullable string, enum, minLength, maxLength, pattern", - "content": { - "application/json": { - "schema": { - "type": "string", - "minLength": 5, - "maxLength": 10, - "pattern": "[A-Za-z]+", - "enum": [ - "a", - "b", - "c" - ], - "nullable": true - } - } - } - }, - "230": { - "description": "bool", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "231": { - "description": "nullable bool", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "nullable": true - } - } - } - }, - "232": { - "description": "bool, enum", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "enum": [ - true - ] - } - } - } - }, - "239": { - "description": "nullable bool, enum", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "enum": [ - true, - null - ], - "nullable": true - } - } - } - }, - "240": { - "description": "array of ints", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "integer" - } - } - } - } - }, - "241": { - "description": "array of strings, enum", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - }, - "enum": [ - [ - "a", - "b", - "c" - ], - [ - "d", - "e", - "f" - ] - ] - } - } - } - }, - "242": { - "description": "nullable array of strings", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - } - } - } - }, - "243": { - "description": "array of booleans, minItems", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "boolean" - }, - "minItems": 5 - } - } - } - }, - "244": { - "description": "array of floats, maxItems", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "number", - "format": "double" - }, - "maxItems": 5 - } - } - } - }, - "245": { - "description": "array of numbers, maxItems", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "number" - }, - "uniqueItems": true - } - } - } - }, - "269": { - "description": "nullable array of nullable numbers, minItems, maxItems, uniqueItems", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "number", - "nullable": true - }, - "enum": [ - [ - 1, - 2.0, - null - ], - [ - 4.0, - null, - 6 - ] - ], - "uniqueItems": true, - "minItems": 2, - "maxItems": 5, - "nullable": true - } - } - } - }, - "270": { - "description": "object with (string) name", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - } - } - } - }, - "271": { - "description": "object with (integer) id, enum", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - }, - "enum": [ - { - "id": 5 - }, - { - "id": 10 - } - ] - } - } - } - }, - "272": { - "description": "nullable object with (float) price", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "price": { - "type": "number", - "format": "float" - } - }, - "nullable": true - } - } - } - }, - "273": { - "description": "object with (string) name, (int) id, (bool) status", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "status": { - "type": "boolean" - } - } - } - } - } - }, - "274": { - "description": "object with (string) name, (int) id, (bool) status, required", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "status": { - "type": "boolean" - } - }, - "required": [ - "name", - "id" - ] - } - } - } - }, - "299": { - "description": "nullable object with (string) name, (int) id, (bool) status, enum, required", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "status": { - "type": "boolean" - } - }, - "enum": [ - { - "name": "Ben", - "id": 5, - "status": true - }, - { - "name": "Blink", - "id": 10, - "status": true - } - ], - "required": [ - "name", - "id" - ] - } - } - } - }, - "300": { - "description": "allOf, one object", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - } - ] - } - } - } - }, - "301": { - "description": "allOf,two objects, identical parameter", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - } - ] - } - } - } - }, - "302": { - "description": "allOf, two objects, unique parameters", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - } - ] - } - } - } - }, - "303": { - "description": "allOf, two objects, conflicting parameters", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - } - } - } - ] - } - } - } - }, - "304": { - "description": "allOf, two objects, unique parameters, one requiredField", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "name" - ] - } - ] - } - } - } - }, - "305": { - "description": "allOf, two objects, unique parameters, two requiredFields", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - }, - "required": [ - "id" - ] - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "name" - ] - } - ] - } - } - } - }, - "306": { - "description": "allOf, two objects, unique parameters, two requiredFields requiring the other schemas property", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - }, - "required": [ - "name" - ] - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "id" - ] - } - ] - } - } - } - }, - "320": { - "description": "anyOf, one object", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - } - ] - } - } - } - }, - "321": { - "description": "anyOf,two objects, identical parameter", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - } - ] - } - } - } - }, - "322": { - "description": "anyOf, two objects, unique parameters", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - } - ] - } - } - } - }, - "323": { - "description": "anyOf, two objects, conflicting parameters", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - } - } - } - ] - } - } - } - }, - "324": { - "description": "anyOf, two objects, unique parameters, one requiredField", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "name" - ] - } - ] - } - } - } - }, - "325": { - "description": "anyOf, two objects, unique parameters, two requiredFields", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - }, - "required": [ - "id" - ] - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "name" - ] - } - ] - } - } - } - }, - "326": { - "description": "anyOf, two objects, unique parameters, two requiredFields requiring the other schemas property", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - }, - "required": [ - "name" - ] - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "id" - ] - } - ] - } - } - } - }, - "340": { - "description": "oneOf, one object", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - } - ] - } - } - } - }, - "341": { - "description": "oneOf,two objects, identical parameter", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - } - ] - } - } - } - }, - "342": { - "description": "oneOf, two objects, unique parameters", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - } - ] - } - } - } - }, - "343": { - "description": "oneOf, two objects, conflicting parameters", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - } - } - } - ] - } - } - } - }, - "344": { - "description": "oneOf, two objects, unique parameters, one requiredField", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "name" - ] - } - ] - } - } - } - }, - "345": { - "description": "oneOf, two objects, unique parameters, two requiredFields", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - }, - "required": [ - "id" - ] - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "name" - ] - } - ] - } - } - } - }, - "346": { - "description": "oneOf, two objects, unique parameters, two requiredFields requiring the other schemas property", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - }, - "required": [ - "name" - ] - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "id" - ] - } - ] - } - } - } - }, - "360": { - "description": "'not' a string", - "content": { - "application/json": { - "schema": { - "not": { - "type": "string" - } - } - } - } - }, - "404": { - "description": "schema with no specified type", - "content": { - "application/json": { - "schema": { - } - } - } - } - } - } - } - } -} diff --git a/tests/fixtures/references.json b/tests/fixtures/references.json deleted file mode 100644 index 73e2f5a..0000000 --- a/tests/fixtures/references.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Test API", - "version": "1.0.0" - }, - "servers": [ - { - "url": "http://test.com" - } - ], - "paths": { - "/path": { - "get": { - "parameters": [ - { - "name": "id", - "in": "header", - "required": true, - "schema": { - "$ref": "#/components/schemas/id" - } - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/200" - } - } - } - } - }, - "components": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "integer" - } - } - } - } - }, - "schemas": { - "id": { - "type": "integer" - } - } - } -} diff --git a/tests/fixtures/references.yaml b/tests/fixtures/references.yaml deleted file mode 100644 index d4311db..0000000 --- a/tests/fixtures/references.yaml +++ /dev/null @@ -1,30 +0,0 @@ ---- -openapi: 3.0.0 -info: - title: Test API - version: 1.0.0 -servers: - - url: http://test.com -paths: - "/path": - get: - parameters: - - name: id - in: header - required: true - schema: - "$ref": "#/components/schemas/id" - responses: - '200': - "$ref": "#/components/responses/200" -components: - responses: - '200': - description: Successful Response - content: - application/json: - schema: - type: integer - schemas: - id: - type: integer diff --git a/tests/fixtures/simple.json b/tests/fixtures/simple.json deleted file mode 100644 index 6d03897..0000000 --- a/tests/fixtures/simple.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Test API", - "version": "1.0.0" - }, - "paths": { - "/path": { - } - } -}