From 76537bc2f85e9669382a62e2d881e7f46a3074ed Mon Sep 17 00:00:00 2001 From: John Charman Date: Mon, 11 Nov 2024 16:36:43 +0000 Subject: [PATCH 1/3] Temporarily Depend on forked branch Temporary workaround while adding support for OpenAPI 3.1. Temporarily lower min-stability to dev. --- composer.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 7297405..d08064c 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ }, "require": { "php": "^8.1.0", - "membrane/openapi-reader": "2.1.0", + "membrane/openapi-reader": "dev-router-mvp-3.1-support@dev", "psr/http-message": "^1.0 || ^2.0", "psr/log": "^3.0", "symfony/console": "^6.0 || ^7.0" @@ -35,5 +35,11 @@ "allow-plugins": { "infection/extension-installer": true } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/charjr/php-openapi" + } + ] } From fc1ba20f3b4bc09ab4585bed1a503b90517785f8 Mon Sep 17 00:00:00 2001 From: John Charman Date: Mon, 11 Nov 2024 18:06:39 +0000 Subject: [PATCH 2/3] Test support for OpenAPI 3.1 --- src/Console/Service/CacheOpenAPIRoutes.php | 6 +- src/RouteCollector.php | 4 +- tests/RouteCollectorTest.php | 286 +---- tests/RouterTest.php | 50 +- tests/fixtures/ProvidesAPIeceOfCake.php | 87 ++ tests/fixtures/ProvidesApiAndRoutes.php | 199 +-- tests/fixtures/ProvidesPetstoreExpanded.php | 58 + tests/fixtures/ProvidesTrainTravel.php | 64 + tests/fixtures/ProvidesWeirdAndWonderful.php | 145 +++ tests/fixtures/train-travel-api.yaml | 1188 ++++++++++++++++++ 10 files changed, 1627 insertions(+), 460 deletions(-) create mode 100644 tests/fixtures/ProvidesAPIeceOfCake.php create mode 100644 tests/fixtures/ProvidesPetstoreExpanded.php create mode 100644 tests/fixtures/ProvidesTrainTravel.php create mode 100644 tests/fixtures/ProvidesWeirdAndWonderful.php create mode 100644 tests/fixtures/train-travel-api.yaml diff --git a/src/Console/Service/CacheOpenAPIRoutes.php b/src/Console/Service/CacheOpenAPIRoutes.php index 6c6f966..593f49d 100644 --- a/src/Console/Service/CacheOpenAPIRoutes.php +++ b/src/Console/Service/CacheOpenAPIRoutes.php @@ -36,8 +36,10 @@ public function cache( } try { - $openApi = (new MembraneReader([OpenAPIVersion::Version_3_0])) - ->readFromAbsoluteFilePath($openAPIFilePath); + $openApi = (new MembraneReader([ + OpenAPIVersion::Version_3_0, + OpenAPIVersion::Version_3_1 + ]))->readFromAbsoluteFilePath($openAPIFilePath); } catch (CannotRead $e) { $this->logger->error($e->getMessage()); return false; diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 9f19148..79abd8d 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -4,13 +4,13 @@ namespace Membrane\OpenAPIRouter; -use Membrane\OpenAPIReader\ValueObject\Valid\V30\OpenAPI; +use Membrane\OpenAPIReader\ValueObject\Valid\{V30, V31}; use Membrane\OpenAPIRouter\Exception\CannotCollectRoutes; use Membrane\OpenAPIRouter\Route\Server; class RouteCollector { - public function collect(OpenAPI $openApi): RouteCollection + public function collect(V30\OpenAPI|V31\OpenAPI $openApi): RouteCollection { $collection = []; diff --git a/tests/RouteCollectorTest.php b/tests/RouteCollectorTest.php index cb1b2f0..a9c9f5e 100644 --- a/tests/RouteCollectorTest.php +++ b/tests/RouteCollectorTest.php @@ -12,8 +12,10 @@ use Membrane\OpenAPIRouter\Route; use Membrane\OpenAPIRouter\RouteCollection; use Membrane\OpenAPIRouter\RouteCollector; +use Membrane\OpenAPIRouter\Tests\Fixtures\ProvidesApiAndRoutes; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; @@ -24,20 +26,15 @@ #[UsesClass(RouteCollection::class)] class RouteCollectorTest extends TestCase { - public const FIXTURES = __DIR__ . '/fixtures/'; - #[Test] - public function throwExceptionIfThereAreNoRoutes(): void + public function itThrowsIfNoRoutes(): void { $openAPI = (new MembraneReader([OpenAPIVersion::Version_3_0])) - ->readFromString( - json_encode([ - 'openapi' => '3.0.0', - 'info' => ['title' => '', 'version' => '1.0.0'], - 'paths' => [] - ]), - FileFormat::Json - ); + ->readFromString(json_encode([ + 'openapi' => '3.0.0', + 'info' => ['title' => '', 'version' => '1.0.0'], + 'paths' => [] + ]), FileFormat::Json); self::expectExceptionObject(CannotCollectRoutes::noRoutes()); @@ -45,264 +42,35 @@ public function throwExceptionIfThereAreNoRoutes(): void } #[Test] - public function removesDuplicateServers(): void + public function itRemovesDuplicateServers(): void { $openAPI = (new MembraneReader([OpenAPIVersion::Version_3_0])) - ->readFromString( - json_encode([ - 'openapi' => '3.0.0', - 'info' => ['title' => '', 'version' => '1.0.0'], - 'servers' => [ - ['url' => 'https://www.server.net'], - ['url' => 'https://www.server.net/'], - ], - 'paths' => [ - '/path' => [ - 'get' => [ - 'operationId' => 'get-path', - 'responses' => [200 => ['description' => 'Successful Response']] - ] - ] - ] - ]), - FileFormat::Json - ); + ->readFromString(json_encode([ + 'openapi' => '3.0.0', + 'info' => ['title' => '', 'version' => '1.0.0'], + 'servers' => [ + ['url' => 'https://www.server.net'], + ['url' => 'https://www.server.net/'], + ], + 'paths' => ['/path' => ['get' => [ + 'operationId' => 'get-path', + 'responses' => [200 => ['description' => 'Successful Response']] + ]]] + ]), FileFormat::Json); $routeCollection = (new RouteCollector())->collect($openAPI); self::assertCount(1, $routeCollection->routes['hosted']['static']); } - 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 + #[DataProviderExternal(ProvidesApiAndRoutes::class, 'defaultBehaviour')] + public function collectTest(string $apiFilePath, RouteCollection $expected): void { - $openAPI = (new MembraneReader([OpenAPIVersion::Version_3_0])) - ->readFromAbsoluteFilePath($apiFilePath); + $openAPI = (new MembraneReader([ + OpenAPIVersion::Version_3_0, + OpenAPIVersion::Version_3_1, + ]))->readFromAbsoluteFilePath($apiFilePath); self::assertEquals($expected, (new RouteCollector())->collect($openAPI)); } diff --git a/tests/RouterTest.php b/tests/RouterTest.php index e0e068f..d8a3ac0 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -13,6 +13,8 @@ use Membrane\OpenAPIRouter\RouteCollection; use Membrane\OpenAPIRouter\RouteCollector; use Membrane\OpenAPIRouter\Router; +use Membrane\OpenAPIRouter\Tests\Fixtures\ProvidesPetstoreExpanded; +use Membrane\OpenAPIRouter\Tests\Fixtures\ProvidesWeirdAndWonderful; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; @@ -27,32 +29,6 @@ #[UsesClass(Route\Path::class), UsesClass(Route\Server::class)] class RouterTest extends TestCase { - private const FIXTURES = __DIR__ . '/fixtures/'; - - private static function getPetStoreRouteCollection(): RouteCollection - { - $openAPI = (new MembraneReader([OpenAPIVersion::Version_3_0])) - ->readFromAbsoluteFilePath(self::FIXTURES . 'docs/petstore-expanded.json'); - - return (new RouteCollector())->collect($openAPI); - } - - private static function getWeirdAndWonderfulRouteCollection(): RouteCollection - { - $openAPI = (new MembraneReader([OpenAPIVersion::Version_3_0])) - ->readFromAbsoluteFilePath(self::FIXTURES . 'WeirdAndWonderful.json'); - - return (new RouteCollector())->collect($openAPI); - } - - private static function getAPieceOfCakeRouteCollection(): RouteCollection - { - $openAPI = (new MembraneReader([OpenAPIVersion::Version_3_0])) - ->readFromAbsoluteFilePath(self::FIXTURES . 'APIeceOfCake.json'); - - return (new RouteCollector())->collect($openAPI); - } - public static function unsuccessfulRouteProvider(): array { return [ @@ -60,25 +36,25 @@ public static function unsuccessfulRouteProvider(): array Exception\CannotRouteRequest::notFound(), 'https://hatshop.dapper.net/api/pets', 'get', - self::getPetStoreRouteCollection(), + ProvidesPetstoreExpanded::getRoutes(), ], 'petstore-expanded: correct static server url but incorrect path' => [ Exception\CannotRouteRequest::notFound(), 'http://petstore.swagger.io/api/hats', 'get', - self::getPetStoreRouteCollection(), + ProvidesPetstoreExpanded::getRoutes(), ], 'WeirdAndWonderful: correct dynamic erver url but incorrect path' => [ Exception\CannotRouteRequest::notFound(), 'http://weird.io/however/but', 'get', - self::getWeirdAndWonderfulRouteCollection(), + ProvidesWeirdAndWonderful::getRoutes(), ], 'petstore-expanded: correct url but incorrect method' => [ Exception\CannotRouteRequest::methodNotAllowed(), 'http://petstore.swagger.io/api/pets', 'delete', - self::getPetStoreRouteCollection(), + ProvidesPetstoreExpanded::getRoutes(), ], ]; } @@ -105,43 +81,43 @@ public static function successfulRouteProvider(): array 'findPets', 'http://petstore.swagger.io/api/pets', 'get', - self::getPetStoreRouteCollection(), + ProvidesPetstoreExpanded::getRoutes(), ], 'petstore: /pets/{id} path, get method' => [ 'find pet by id', 'http://petstore.swagger.io/api/pets/1', 'get', - self::getPetStoreRouteCollection(), + ProvidesPetstoreExpanded::getRoutes(), ], 'petstore: /pets/{id} path, delete method' => [ 'deletePet', 'http://petstore.swagger.io/api/pets/1', 'delete', - self::getPetStoreRouteCollection(), + ProvidesPetstoreExpanded::getRoutes(), ], 'WeirdAndWonderful: /v1/or path, post method' => [ 'post-or', '/v1/or', 'post', - self::getWeirdAndWonderfulRouteCollection(), + ProvidesWeirdAndWonderful::getRoutes(), ], 'WeirdAndWonderful: http://www.arbitrary.com/v1/or path, post method' => [ 'post-or', '/v1/or', 'post', - self::getWeirdAndWonderfulRouteCollection(), + ProvidesWeirdAndWonderful::getRoutes(), ], 'WeirdAndWonderful: http://weird.io/however/or path, post method' => [ 'post-or', 'http://weird.io/however/or', 'post', - self::getWeirdAndWonderfulRouteCollection(), + ProvidesWeirdAndWonderful::getRoutes(), ], 'WeirdAndWonderful: /{version}/xor path, delete method' => [ 'delete-xor', '/12/xor', 'delete', - self::getWeirdAndWonderfulRouteCollection(), + ProvidesWeirdAndWonderful::getRoutes(), ], ]; } diff --git a/tests/fixtures/ProvidesAPIeceOfCake.php b/tests/fixtures/ProvidesAPIeceOfCake.php new file mode 100644 index 0000000..ec258f8 --- /dev/null +++ b/tests/fixtures/ProvidesAPIeceOfCake.php @@ -0,0 +1,87 @@ + [ + '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' => []], + ], + ]); + } + + public static function getRoutesIgnoringServers(): RouteCollection + { + return 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' => []], + ], + ]); + } +} diff --git a/tests/fixtures/ProvidesApiAndRoutes.php b/tests/fixtures/ProvidesApiAndRoutes.php index 7138d3c..5ae5eee 100644 --- a/tests/fixtures/ProvidesApiAndRoutes.php +++ b/tests/fixtures/ProvidesApiAndRoutes.php @@ -5,175 +5,54 @@ namespace Membrane\OpenAPIRouter\Tests\Fixtures; use Generator; +use Membrane\OpenAPIReader\Tests\Fixtures\ProvidesTrainTravelApi; use Membrane\OpenAPIRouter\RouteCollection; final class ProvidesApiAndRoutes { - private const PETSTORE_EXPANDED = __DIR__ . '/docs/petstore-expanded.json'; - private const WEIRD_AND_WONDERFUL = __DIR__ . '/WeirdAndWonderful.json'; - public static function defaultBehaviour(): Generator { - yield 'petstore expanded' => [self::PETSTORE_EXPANDED, 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' => []]], - ])]; - - yield 'weird and wonderful' => [self::WEIRD_AND_WONDERFUL, 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' => []], - ]], - ], - ], - ])]; + yield 'petstore-expanded' => [ + ProvidesPetstoreExpanded::getFilePath(), + ProvidesPetstoreExpanded::getRoutes(), + ]; + + yield 'train-travel' => [ + ProvidesTrainTravel::getFilePath(), + ProvidesTrainTravel::getRoutes() + ]; + + yield 'APIece of Cake' => [ + ProvidesAPIeceOfCake::getFilePath(), + ProvidesAPIeceOfCake::getRoutes(), + ]; + + yield 'Weird and Wonderful' => [ + ProvidesWeirdAndWonderful::getFilePath(), + ProvidesWeirdAndWonderful::getRoutes(), + ]; } public static function ignoringServers(): Generator { - yield 'petstore-expanded, ignoring servers' => [self::PETSTORE_EXPANDED, new RouteCollection([ - 'hosted' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], - 'hostless' => [ - 'static' => ['' => [ - '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' => []], - ], - ])]; - - yield 'weird-and-wonderful, ignoring servers' => [self::WEIRD_AND_WONDERFUL, new RouteCollection([ - 'hosted' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], - 'hostless' => [ - 'static' => ['' => [ - 'static' => [ - '/or' => [ - 'post' => 'post-or', - ], - '/xor' => [ - 'delete' => 'delete-xor', - ], - '/however' => [ - 'get' => 'get-however', - 'put' => 'put-however', - 'post' => 'post-however', - ], - ], - 'dynamic' => [ - 'regex' => '#^(?|/and/([^/]+)(*MARK:/and/{name}))$#', - 'paths' => [ - '/and/{name}' => [ - 'get' => 'get-and', - 'put' => 'put-and', - 'post' => 'post-and', - ], - ] - ], - ]], - 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []], - ], - ])]; + yield 'petstore-expanded' => [ + ProvidesPetstoreExpanded::getFilePath(), + ProvidesPetstoreExpanded::getRoutesIgnoringServers(), + ]; + + yield 'train-travel' => [ + ProvidesTrainTravel::getFilePath(), + ProvidesTrainTravel::getRoutesIgnoringServers() + ]; + + yield 'APIece of Cake' => [ + ProvidesAPIeceOfCake::getFilePath(), + ProvidesAPIeceOfCake::getRoutesIgnoringServers(), + ]; + + yield 'Weird and Wonderful' => [ + ProvidesWeirdAndWonderful::getFilePath(), + ProvidesWeirdAndWonderful::getRoutesIgnoringServers(), + ]; } } diff --git a/tests/fixtures/ProvidesPetstoreExpanded.php b/tests/fixtures/ProvidesPetstoreExpanded.php new file mode 100644 index 0000000..e4d009a --- /dev/null +++ b/tests/fixtures/ProvidesPetstoreExpanded.php @@ -0,0 +1,58 @@ + [ + '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' => []]], + ]); + } + + public static function getRoutesIgnoringServers(): RouteCollection + { + return new RouteCollection([ + 'hosted' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], + 'hostless' => [ + 'static' => ['' => [ + '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' => []], + ], + ]); + } +} diff --git a/tests/fixtures/ProvidesTrainTravel.php b/tests/fixtures/ProvidesTrainTravel.php new file mode 100644 index 0000000..52b24b0 --- /dev/null +++ b/tests/fixtures/ProvidesTrainTravel.php @@ -0,0 +1,64 @@ + [ + 'static' => ['https://api.example.com' => [ + 'static' => [ + '/stations' => ['get' => 'get-stations'], + '/trips' => ['get' => 'get-trips'], + '/bookings' => ['get' => 'get-bookings', 'post' => 'create-booking'], + ], + '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'], + ], + ], + ]], + 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []], + ], + 'hostless' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], + ]); + } + + 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'], + ], + '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'], + ], + ], + ]], + 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []], + ], + ]); + } +} diff --git a/tests/fixtures/ProvidesWeirdAndWonderful.php b/tests/fixtures/ProvidesWeirdAndWonderful.php new file mode 100644 index 0000000..ce33097 --- /dev/null +++ b/tests/fixtures/ProvidesWeirdAndWonderful.php @@ -0,0 +1,145 @@ + [ + '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 getRoutesIgnoringServers(): RouteCollection + { + return new RouteCollection([ + 'hosted' => ['static' => [], 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []]], + 'hostless' => [ + 'static' => ['' => [ + 'static' => [ + '/or' => [ + 'post' => 'post-or', + ], + '/xor' => [ + 'delete' => 'delete-xor', + ], + '/however' => [ + 'get' => 'get-however', + 'put' => 'put-however', + 'post' => 'post-however', + ], + ], + 'dynamic' => [ + 'regex' => '#^(?|/and/([^/]+)(*MARK:/and/{name}))$#', + 'paths' => [ + '/and/{name}' => [ + 'get' => 'get-and', + 'put' => 'put-and', + 'post' => 'post-and', + ], + ] + ], + ]], + 'dynamic' => ['regex' => '#^(?|)#', 'servers' => []], + ], + ]); + } +} diff --git a/tests/fixtures/train-travel-api.yaml b/tests/fixtures/train-travel-api.yaml new file mode 100644 index 0000000..49b86c3 --- /dev/null +++ b/tests/fixtures/train-travel-api.yaml @@ -0,0 +1,1188 @@ +openapi: 3.1.0 +info: + title: Train Travel API + description: | + API for finding and booking train trips across Europe. + + ## Run in Postman + + Experiment with this API in Postman, using our Postman Collection. + + [Run In Postman](https://app.getpostman.com/run-collection/9265903-7a75a0d0-b108-4436-ba54-c6139698dc08?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D9265903-7a75a0d0-b108-4436-ba54-c6139698dc08%26entityType%3Dcollection%26workspaceId%3Df507f69d-9564-419c-89a2-cb8e4c8c7b8f) + version: 1.0.0 + contact: + name: Train Support + url: https://example.com/support + email: support@example.com + license: + name: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International + identifier: CC-BY-NC-SA-4.0 + x-feedbackLink: + label: Submit Feedback + url: https://github.com/bump-sh-examples/train-travel-api/issues/new + +servers: + - url: https://api.example.com + description: Production + +security: + - OAuth2: + - read + +x-topics: + - title: Getting started + content: + $ref: ./docs/getting-started.md + +tags: + - name: Stations + description: | + Find and filter train stations across Europe, including their location + and local timezone. + - name: Trips + description: | + Timetables and routes for train trips between stations, including pricing + and availability. + - name: Bookings + description: | + Create and manage bookings for train trips, including passenger details + and optional extras. + - name: Payments + description: | + Pay for bookings using a card or bank account, and view payment + status and history. + + > warn + > Bookings usually expire within 1 hour so you'll need to make your payment + > before the expiry date + +paths: + /stations: + get: + summary: Get a list of train stations + description: Returns a paginated and searchable list of all train stations. + operationId: get-stations + tags: + - Stations + parameters: + - name: page + in: query + description: The page number to return + required: false + schema: + type: integer + minimum: 1 + default: 1 + example: 1 + - name: coordinates + in: query + description: > + The latitude and longitude of the user's location, to narrow down + the search results to sites within a proximity of this location. + required: false + schema: + type: string + example: 52.5200,13.4050 + - name: search + in: query + description: > + A search term to filter the list of stations by name or address. + required: false + schema: + type: string + examples: + - Milano Centrale + - Paris + responses: + '200': + description: OK + headers: + Cache-Control: + $ref: '#/components/headers/Cache-Control' + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Station' + example: + data: + - id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + name: Berlin Hauptbahnhof + address: Invalidenstraße 10557 Berlin, Germany + country_code: DE + timezone: Europe/Berlin + - id: b2e783e1-c824-4d63-b37a-d8d698862f1d + name: Paris Gare du Nord + address: 18 Rue de Dunkerque 75010 Paris, France + country_code: FR + timezone: Europe/Paris + links: + self: https://api.example.com/stations&page=2 + next: https://api.example.com/stations?page=3 + prev: https://api.example.com/stations?page=1 + application/xml: + schema: + allOf: + - $ref: '#/components/schemas/Wrapper-Collection' + - properties: + data: + type: array + xml: + name: stations + wrapped: true + items: + $ref: '#/components/schemas/Station' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + /trips: + get: + summary: Get available train trips + description: > + Returns a list of available train trips between the specified origin and + destination stations on the given date, and allows for filtering by + bicycle and dog allowances. + operationId: get-trips + tags: + - Trips + parameters: + - name: origin + in: query + description: The ID of the origin station + required: true + schema: + type: string + format: uuid + example: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + - name: destination + in: query + description: The ID of the destination station + required: true + schema: + type: string + format: uuid + example: b2e783e1-c824-4d63-b37a-d8d698862f1d + - name: date + in: query + description: The date and time of the trip in ISO 8601 format in origin station's timezone. + required: true + schema: + type: string + format: date-time + example: '2024-02-01T09:00:00Z' + - name: bicycles + in: query + description: Only return trips where bicycles are known to be allowed + required: false + schema: + type: boolean + default: false + - name: dogs + in: query + description: Only return trips where dogs are known to be allowed + required: false + schema: + type: boolean + default: false + - name: page + in: query + description: The page of results to be returned + required: false + schema: + type: number + default: 1 + responses: + '200': + description: A list of available train trips + headers: + Cache-Control: + $ref: '#/components/headers/Cache-Control' + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Wrapper-Collection' + - properties: + data: + type: array + items: + $ref: '#/components/schemas/Trip' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + example: + data: + - id: ea399ba1-6d95-433f-92d1-83f67b775594 + origin: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + destination: b2e783e1-c824-4d63-b37a-d8d698862f1d + departure_time: '2024-02-01T10:00:00Z' + arrival_time: '2024-02-01T16:00:00Z' + price: 50 + operator: Deutsche Bahn + bicycles_allowed: true + dogs_allowed: true + - id: 4d67459c-af07-40bb-bb12-178dbb88e09f + origin: b2e783e1-c824-4d63-b37a-d8d698862f1d + destination: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + departure_time: '2024-02-01T12:00:00Z' + arrival_time: '2024-02-01T18:00:00Z' + price: 50 + operator: SNCF + bicycles_allowed: true + dogs_allowed: true + links: + self: https://api.example.com/trips?origin=efdbb9d1-02c2-4bc3-afb7-6788d8782b1e&destination=b2e783e1-c824-4d63-b37a-d8d698862f1d&date=2024-02-01 + next: https://api.example.com/trips?origin=efdbb9d1-02c2-4bc3-afb7-6788d8782b1e&destination=b2e783e1-c824-4d63-b37a-d8d698862f1d&date=2024-02-01&page=2 + application/xml: + schema: + allOf: + - $ref: '#/components/schemas/Wrapper-Collection' + - properties: + data: + type: array + xml: + name: trips + wrapped: true + items: + $ref: '#/components/schemas/Trip' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + /bookings: + get: + operationId: get-bookings + summary: List existing bookings + description: Returns a list of all trip bookings by the authenticated user. + tags: + - Bookings + parameters: + - name: page + in: query + description: The page number to return + required: false + schema: + type: integer + minimum: 1 + default: 1 + example: 1 + responses: + '200': + description: A list of bookings + headers: + Cache-Control: + $ref: '#/components/headers/Cache-Control' + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Wrapper-Collection' + - properties: + data: + type: array + items: + $ref: '#/components/schemas/Booking' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + example: + data: + - id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + trip_id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + passenger_name: John Doe + has_bicycle: true + has_dog: true + - id: b2e783e1-c824-4d63-b37a-d8d698862f1d + trip_id: b2e783e1-c824-4d63-b37a-d8d698862f1d + passenger_name: Jane Smith + has_bicycle: false + has_dog: false + links: + self: https://api.example.com/bookings + next: https://api.example.com/bookings?page=2 + application/xml: + schema: + allOf: + - $ref: '#/components/schemas/Wrapper-Collection' + - properties: + data: + type: array + xml: + name: bookings + wrapped: true + items: + $ref: '#/components/schemas/Booking' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + post: + operationId: create-booking + summary: Create a booking + description: A booking is a temporary hold on a trip. It is not confirmed until the payment is processed. + tags: + - Bookings + security: + - OAuth2: + - write + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Booking' + application/xml: + schema: + $ref: '#/components/schemas/Booking' + responses: + '201': + description: Booking successful + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Booking' + - properties: + links: + $ref: '#/components/schemas/Links-Self' + + example: + id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + trip_id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + passenger_name: John Doe + has_bicycle: true + has_dog: true + links: + self: https://api.example.com/bookings/efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + application/xml: + schema: + allOf: + - $ref: '#/components/schemas/Booking' + - properties: + links: + $ref: '#/components/schemas/Links-Self' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + + /bookings/{bookingId}: + parameters: + - name: bookingId + in: path + required: true + description: The ID of the booking to retrieve. + schema: + type: string + format: uuid + example: 1725ff48-ab45-4bb5-9d02-88745177dedb + get: + summary: Get a booking + description: Returns the details of a specific booking. + operationId: get-booking + tags: + - Bookings + responses: + '200': + description: The booking details + headers: + Cache-Control: + $ref: '#/components/headers/Cache-Control' + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Booking' + - properties: + links: + $ref: '#/components/schemas/Links-Self' + example: + id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + trip_id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + passenger_name: John Doe + has_bicycle: true + has_dog: true + links: + self: https://api.example.com/bookings/1725ff48-ab45-4bb5-9d02-88745177dedb + application/xml: + schema: + allOf: + - $ref: '#/components/schemas/Booking' + - properties: + links: + $ref: '#/components/schemas/Links-Self' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + delete: + summary: Delete a booking + description: Deletes a booking, cancelling the hold on the trip. + operationId: delete-booking + security: + - OAuth2: + - write + tags: + - Bookings + responses: + '204': + description: Booking deleted + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + + /bookings/{bookingId}/payment: + parameters: + - name: bookingId + in: path + required: true + description: The ID of the booking to pay for. + schema: + type: string + format: uuid + example: 1725ff48-ab45-4bb5-9d02-88745177dedb + post: + summary: Pay for a Booking + description: A payment is an attempt to pay for the booking, which will confirm the booking for the user and enable them to get their tickets. + operationId: create-booking-payment + tags: + - Payments + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BookingPayment' + examples: + Card: + summary: Card Payment + value: + amount: 49.99 + currency: gbp + source: + object: card + name: J. Doe + number: '4242424242424242' + cvc: 123 + exp_month: 12 + exp_year: 2025 + address_line1: 123 Fake Street + address_line2: 4th Floor + address_city: London + address_country: gb + address_post_code: N12 9XX + Bank: + summary: Bank Account Payment + value: + amount: 100.5 + currency: gbp + source: + object: bank_account + name: J. Doe + number: '00012345' + sort_code: '000123' + account_type: individual + bank_name: Starling Bank + country: gb + responses: + '200': + description: Payment successful + headers: + Cache-Control: + $ref: '#/components/headers/Cache-Control' + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/BookingPayment' + - properties: + links: + $ref: '#/components/schemas/Links-Booking' + examples: + Card: + summary: Card Payment + value: + id: 2e3b4f5a-6b7c-8d9e-0f1a-2b3c4d5e6f7a + amount: 49.99 + currency: gbp + source: + object: card + name: J. Doe + number: '************4242' + cvc: 123 + exp_month: 12 + exp_year: 2025 + address_country: gb + address_post_code: N12 9XX + status: succeeded + links: + booking: https://api.example.com/bookings/1725ff48-ab45-4bb5-9d02-88745177dedb/payment + Bank: + summary: Bank Account Payment + value: + id: 2e3b4f5a-6b7c-8d9e-0f1a-2b3c4d5e6f7a + amount: 100.5 + currency: gbp + source: + object: bank_account + name: J. Doe + account_type: individual + number: '*********2345' + sort_code: '000123' + bank_name: Starling Bank + country: gb + status: succeeded + links: + booking: https://api.example.com/bookings/1725ff48-ab45-4bb5-9d02-88745177dedb + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' +webhooks: + newBooking: + post: + operationId: new-booking + summary: New Booking + description: | + Subscribe to new bookings being created, to update integrations for your users. Related data is available via the links provided in the request. + tags: + - Bookings + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Booking' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + example: + id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + trip_id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + passenger_name: John Doe + has_bicycle: true + has_dog: true + links: + self: https://api.example.com/bookings/1725ff48-ab45-4bb5-9d02-88745177dedb + responses: + '200': + description: Return a 200 status to indicate that the data was received successfully. + +components: + securitySchemes: + OAuth2: + type: oauth2 + description: OAuth 2.0 authorization code following RFC8725 best practices. + flows: + authorizationCode: + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + scopes: + read: Read access + write: Write access + schemas: + Station: + type: object + xml: + name: station + required: + - id + - name + - address + - country_code + properties: + id: + type: string + format: uuid + description: Unique identifier for the station. + examples: + - efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + - b2e783e1-c824-4d63-b37a-d8d698862f1d + name: + type: string + description: The name of the station + examples: + - Berlin Hauptbahnhof + - Paris Gare du Nord + address: + type: string + description: The address of the station. + examples: + - Invalidenstraße 10557 Berlin, Germany + - 18 Rue de Dunkerque 75010 Paris, France + country_code: + type: string + description: The country code of the station. + format: iso-country-code + examples: + - DE + - FR + timezone: + type: string + description: The timezone of the station in the [IANA Time Zone Database format](https://www.iana.org/time-zones). + examples: + - Europe/Berlin + - Europe/Paris + Links-Self: + type: object + properties: + self: + type: string + format: uri + Links-Pagination: + type: object + properties: + next: + type: string + format: uri + prev: + type: string + format: uri + Problem: + type: object + xml: + name: problem + namespace: urn:ietf:rfc:7807 + properties: + type: + type: string + description: A URI reference that identifies the problem type + example: https://example.com/probs/out-of-credit + title: + type: string + description: A short, human-readable summary of the problem type + example: You do not have enough credit. + detail: + type: string + description: A human-readable explanation specific to this occurrence of the problem + example: Your current balance is 30, but that costs 50. + instance: + type: string + description: A URI reference that identifies the specific occurrence of the problem + example: /account/12345/msgs/abc + status: + type: integer + description: The HTTP status code + example: 400 + Trip: + type: object + xml: + name: trip + properties: + id: + type: string + format: uuid + description: Unique identifier for the trip + examples: + - 4f4e4e1-c824-4d63-b37a-d8d698862f1d + origin: + type: string + description: The starting station of the trip + examples: + - Berlin Hauptbahnhof + - Paris Gare du Nord + destination: + type: string + description: The destination station of the trip + examples: + - Paris Gare du Nord + - Berlin Hauptbahnhof + departure_time: + type: string + format: date-time + description: The date and time when the trip departs + examples: + - '2024-02-01T10:00:00Z' + arrival_time: + type: string + format: date-time + description: The date and time when the trip arrives + examples: + - '2024-02-01T16:00:00Z' + operator: + type: string + description: The name of the operator of the trip + examples: + - Deutsche Bahn + - SNCF + price: + type: number + description: The cost of the trip + examples: + - 50 + bicycles_allowed: + type: boolean + description: Indicates whether bicycles are allowed on the trip + dogs_allowed: + type: boolean + description: Indicates whether dogs are allowed on the trip + Booking: + type: object + xml: + name: booking + properties: + id: + type: string + format: uuid + description: Unique identifier for the booking + readOnly: true + examples: + - 3f3e3e1-c824-4d63-b37a-d8d698862f1d + trip_id: + type: string + format: uuid + description: Identifier of the booked trip + examples: + - 4f4e4e1-c824-4d63-b37a-d8d698862f1d + passenger_name: + type: string + description: Name of the passenger + examples: + - John Doe + has_bicycle: + type: boolean + description: Indicates whether the passenger has a bicycle. + has_dog: + type: boolean + description: Indicates whether the passenger has a dog. + Wrapper-Collection: + description: This is a generic request/response wrapper which contains both data and links which serve as hypermedia controls (HATEOAS). + type: object + properties: + data: + description: The wrapper for a collection is an array of objects. + type: array + items: + type: object + links: + description: A set of hypermedia links which serve as controls for the client. + type: object + readOnly: true + xml: + name: data + BookingPayment: + type: object + properties: + id: + description: Unique identifier for the payment. This will be a unique identifier for the payment, and is used to reference the payment in other objects. + type: string + format: uuid + readOnly: true + amount: + description: Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. + type: number + exclusiveMinimum: 0 + examples: + - 49.99 + currency: + description: Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. + type: string + enum: + - bam + - bgn + - chf + - eur + - gbp + - nok + - sek + - try + source: + unevaluatedProperties: false + description: The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking. + oneOf: + - title: Card + description: A card (debit or credit) to take payment from. + type: object + properties: + object: + type: string + const: card + name: + type: string + description: Cardholder's full name as it appears on the card. + examples: + - Francis Bourgeois + number: + type: string + description: The card number, as a string without any separators. On read all but the last four digits will be masked for security. + examples: + - '4242424242424242' + cvc: + type: integer + description: Card security code, 3 or 4 digits usually found on the back of the card. + minLength: 3 + maxLength: 4 + writeOnly: true + + example: 123 + exp_month: + type: integer + format: int64 + description: Two-digit number representing the card's expiration month. + examples: + - 12 + exp_year: + type: integer + format: int64 + description: Four-digit number representing the card's expiration year. + examples: + - 2025 + address_line1: + type: string + writeOnly: true + address_line2: + type: string + writeOnly: true + address_city: + type: string + address_country: + type: string + address_post_code: + type: string + required: + - name + - number + - cvc + - exp_month + - exp_year + - address_country + - title: Bank Account + description: A bank account to take payment from. Must be able to make payments in the currency specified in the payment. + type: object + properties: + object: + const: bank_account + type: string + name: + type: string + number: + type: string + description: The account number for the bank account, in string form. Must be a current account. + sort_code: + type: string + description: The sort code for the bank account, in string form. Must be a six-digit number. + account_type: + enum: + - individual + - company + type: string + description: The type of entity that holds the account. This can be either `individual` or `company`. + bank_name: + type: string + description: The name of the bank associated with the routing number. + examples: + - Starling Bank + country: + type: string + description: Two-letter country code (ISO 3166-1 alpha-2). + required: + - name + - number + - account_type + - bank_name + - country + status: + description: The status of the payment, one of `pending`, `succeeded`, or `failed`. + type: string + enum: + - pending + - succeeded + - failed + readOnly: true + Links-Booking: + type: object + properties: + booking: + type: string + format: uri + examples: + - https://api.example.com/bookings/1725ff48-ab45-4bb5-9d02-88745177dedb + headers: + Cache-Control: + description: | + The Cache-Control header communicates directives for caching mechanisms in both requests and responses. + It is used to specify the caching directives in responses to prevent caches from storing sensitive information. + schema: + type: string + description: A comma-separated list of directives as defined in [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html). + examples: + - max-age=3600 + - max-age=604800, public + - no-store + - no-cache + - private + + RateLimit: + description: | + The RateLimit header communicates quota policies. It contains a `limit` to + convey the expiring limit, `remaining` to convey the remaining quota units, + and `reset` to convey the time window reset time. + schema: + type: string + examples: + - limit=10, remaining=0, reset=10 + + Retry-After: + description: | + The Retry-After header indicates how long the user agent should wait before making a follow-up request. + The value is in seconds and can be an integer or a date in the future. + If the value is an integer, it indicates the number of seconds to wait. + If the value is a date, it indicates the time at which the user agent should make a follow-up request. + schema: + type: string + examples: + integer: + value: '120' + summary: Retry after 120 seconds + date: + value: 'Fri, 31 Dec 2021 23:59:59 GMT' + summary: Retry after the specified date + + responses: + BadRequest: + description: Bad Request + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/bad-request + title: Bad Request + status: 400 + detail: The request is invalid or missing required parameters. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/bad-request + title: Bad Request + status: 400 + detail: The request is invalid or missing required parameters. + + Conflict: + description: Conflict + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/conflict + title: Conflict + status: 409 + detail: There is a conflict with an existing resource. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/conflict + title: Conflict + status: 409 + detail: There is a conflict with an existing resource. + + Forbidden: + description: Forbidden + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/forbidden + title: Forbidden + status: 403 + detail: Access is forbidden with the provided credentials. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/forbidden + title: Forbidden + status: 403 + detail: Access is forbidden with the provided credentials. + + InternalServerError: + description: Internal Server Error + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/internal-server-error + title: Internal Server Error + status: 500 + detail: An unexpected error occurred. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/internal-server-error + title: Internal Server Error + status: 500 + detail: An unexpected error occurred. + + NotFound: + description: Not Found + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/not-found + title: Not Found + status: 404 + detail: The requested resource was not found. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/not-found + title: Not Found + status: 404 + detail: The requested resource was not found. + + TooManyRequests: + description: Too Many Requests + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + Retry-After: + $ref: '#/components/headers/Retry-After' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/too-many-requests + title: Too Many Requests + status: 429 + detail: You have exceeded the rate limit. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/too-many-requests + title: Too Many Requests + status: 429 + detail: You have exceeded the rate limit. + + Unauthorized: + description: Unauthorized + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/unauthorized + title: Unauthorized + status: 401 + detail: You do not have the necessary permissions. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/unauthorized + title: Unauthorized + status: 401 + detail: You do not have the necessary permissions. From 7030bf793b68e8ae099c6244fc4bde9a50c3c72b Mon Sep 17 00:00:00 2001 From: John Charman Date: Mon, 25 Nov 2024 11:33:45 +0000 Subject: [PATCH 3/3] Use devizzent fork again --- composer.json | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index d08064c..e217afd 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ }, "require": { "php": "^8.1.0", - "membrane/openapi-reader": "dev-router-mvp-3.1-support@dev", + "membrane/openapi-reader": "^3.0", "psr/http-message": "^1.0 || ^2.0", "psr/log": "^3.0", "symfony/console": "^6.0 || ^7.0" @@ -23,7 +23,7 @@ "require-dev": { "infection/infection": "^0.27.0", "phpunit/phpunit": "^10.5.38", - "phpstan/phpstan": "^1.12.7", + "phpstan/phpstan": "^2.1.1", "squizlabs/php_codesniffer": "^3.7", "guzzlehttp/psr7": "^2.4", "mikey179/vfsstream": "^1.6.7" @@ -35,11 +35,5 @@ "allow-plugins": { "infection/extension-installer": true } - }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/charjr/php-openapi" - } - ] + } }