From 676bc49dcae0fc926bd0040a70c8b893b3d05139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ber=C3=A1nek?= Date: Fri, 5 Apr 2024 12:52:14 +0200 Subject: [PATCH 01/12] Add CorsPreflightHandlerInterface to enableGlobalPreflight method (#142) Co-authored-by: Martin Beranek --- CHANGELOG.md | 2 ++ src/ApiDecider.php | 3 ++- src/Handlers/CorsPreflightHandler.php | 2 +- src/Handlers/CorsPreflightHandlerInterface.php | 15 +++++++++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 src/Handlers/CorsPreflightHandlerInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 8106ea0..736e287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [Unreleased][unreleased] +#### Added +* CorsPreflightHandlerInterface - resolve multiple service registered handler error #### Added * Button to copy `Body` content in api console diff --git a/src/ApiDecider.php b/src/ApiDecider.php index 533c62f..29a0415 100644 --- a/src/ApiDecider.php +++ b/src/ApiDecider.php @@ -11,6 +11,7 @@ use Tomaj\NetteApi\Handlers\CorsPreflightHandler; use Tomaj\NetteApi\Handlers\DefaultHandler; use Tomaj\NetteApi\RateLimit\RateLimitInterface; +use Tomaj\NetteApi\Handlers\CorsPreflightHandlerInterface; class ApiDecider { @@ -50,7 +51,7 @@ public function getApi(string $method, int $version, string $package, ?string $a return new Api(new EndpointIdentifier($method, $version, $package, $apiAction), new DefaultHandler(), new NoAuthorization()); } - public function enableGlobalPreflight(ApiHandlerInterface $corsHandler = null) + public function enableGlobalPreflight(CorsPreflightHandlerInterface $corsHandler = null) { if (!$corsHandler) { $corsHandler = new CorsPreflightHandler(new Response()); diff --git a/src/Handlers/CorsPreflightHandler.php b/src/Handlers/CorsPreflightHandler.php index 571d9c2..1213798 100644 --- a/src/Handlers/CorsPreflightHandler.php +++ b/src/Handlers/CorsPreflightHandler.php @@ -8,7 +8,7 @@ use Tomaj\NetteApi\Response\JsonApiResponse; use Tomaj\NetteApi\Response\ResponseInterface; -class CorsPreflightHandler extends BaseHandler +class CorsPreflightHandler extends BaseHandler implements CorsPreflightHandlerInterface { private $response; diff --git a/src/Handlers/CorsPreflightHandlerInterface.php b/src/Handlers/CorsPreflightHandlerInterface.php new file mode 100644 index 0000000..e4e87e6 --- /dev/null +++ b/src/Handlers/CorsPreflightHandlerInterface.php @@ -0,0 +1,15 @@ + Date: Thu, 18 Apr 2024 20:07:38 +0200 Subject: [PATCH 02/12] Change 400 to 404 for missing API (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Roman Mátyus --- CHANGELOG.md | 2 ++ src/Handlers/DefaultHandler.php | 2 +- tests/Handler/DefaultHandlerTest.php | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 736e287..23dac68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file. Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## [Unreleased][unreleased] +#### Changed +* [BC] DefaultHandler response code 404 instead 400 #### Added * CorsPreflightHandlerInterface - resolve multiple service registered handler error diff --git a/src/Handlers/DefaultHandler.php b/src/Handlers/DefaultHandler.php index 11ef4d6..598881e 100644 --- a/src/Handlers/DefaultHandler.php +++ b/src/Handlers/DefaultHandler.php @@ -15,6 +15,6 @@ class DefaultHandler extends BaseHandler */ public function handle(array $params): ResponseInterface { - return new JsonApiResponse(IResponse::S400_BAD_REQUEST, ['status' => 'error', 'message' => 'Unknown api endpoint']); + return new JsonApiResponse(IResponse::S404_NOT_FOUND, ['status' => 'error', 'message' => 'Unknown api endpoint']); } } diff --git a/tests/Handler/DefaultHandlerTest.php b/tests/Handler/DefaultHandlerTest.php index 13abd66..e67f8f6 100644 --- a/tests/Handler/DefaultHandlerTest.php +++ b/tests/Handler/DefaultHandlerTest.php @@ -18,7 +18,7 @@ public function testResponse() { $defaultHandler = new DefaultHandler(); $result = $defaultHandler->handle([]); - $this->assertEquals(400, $result->getCode()); + $this->assertEquals(404, $result->getCode()); $this->assertEquals('application/json', $result->getContentType()); $this->assertEquals('utf-8', $result->getCharset()); $this->assertEquals(['status' => 'error', 'message' => 'Unknown api endpoint'], $result->getPayload()); From 1c08b32c301eab2eba39ca96b322b49bac38c75e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20M=C3=A1tyus?= Date: Fri, 19 Apr 2024 14:21:37 +0200 Subject: [PATCH 03/12] Add support for semantic versions (#145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Roman Mátyus --- CHANGELOG.md | 1 + src/ApiDecider.php | 4 +-- src/EndpointIdentifier.php | 9 ++++-- src/EndpointInterface.php | 2 +- src/Handlers/ApiListingHandler.php | 2 +- src/Handlers/OpenApiHandler.php | 2 +- src/Presenters/ApiPresenter.php | 2 +- tests/ApiDeciderTest.php | 14 ++++---- tests/EndpoinIdentifierTest.php | 37 ++++++++++++++++++++-- tests/Handler/ApiListingHandlerTest.php | 12 +++---- tests/Handler/CorsPreflightHandlerTest.php | 4 +-- tests/Handler/DefaultHandlerTest.php | 4 +-- tests/Handler/OpenApiHandlerTest.php | 6 ++-- tests/Presenters/ApiPresenterTest.php | 16 +++++----- 14 files changed, 76 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23dac68..f2cdc4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [Unreleased][unreleased] #### Changed +* [BC] Support for semantic versioning api. Need retype version from `int` to `string`. * [BC] DefaultHandler response code 404 instead 400 #### Added diff --git a/src/ApiDecider.php b/src/ApiDecider.php index 29a0415..feadf96 100644 --- a/src/ApiDecider.php +++ b/src/ApiDecider.php @@ -26,13 +26,13 @@ class ApiDecider * If decider cannot find handler for given handler, returns defaults. * * @param string $method - * @param integer $version + * @param string $version * @param string $package * @param string $apiAction * * @return Api */ - public function getApi(string $method, int $version, string $package, ?string $apiAction = null) + public function getApi(string $method, string $version, string $package, ?string $apiAction = null) { $method = strtoupper($method); $apiAction = $apiAction === '' ? null : $apiAction; diff --git a/src/EndpointIdentifier.php b/src/EndpointIdentifier.php index d806524..e91a102 100644 --- a/src/EndpointIdentifier.php +++ b/src/EndpointIdentifier.php @@ -4,6 +4,8 @@ namespace Tomaj\NetteApi; +use InvalidArgumentException; + class EndpointIdentifier implements EndpointInterface { private $method; @@ -14,9 +16,12 @@ class EndpointIdentifier implements EndpointInterface private $apiAction; - public function __construct(string $method, int $version, string $package, ?string $apiAction = null) + public function __construct(string $method, string $version, string $package, ?string $apiAction = null) { $this->method = strtoupper($method); + if (strpos($version, '/') !== false) { + throw new InvalidArgumentException('Version must have semantic numbering. For example "1", "1.1", "0.13.2" etc.'); + } $this->version = $version; $this->package = $package; $this->apiAction = $apiAction; @@ -27,7 +32,7 @@ public function getMethod(): string return $this->method; } - public function getVersion(): int + public function getVersion(): string { return $this->version; } diff --git a/src/EndpointInterface.php b/src/EndpointInterface.php index bcf0e60..fe4b60b 100644 --- a/src/EndpointInterface.php +++ b/src/EndpointInterface.php @@ -8,7 +8,7 @@ interface EndpointInterface { public function getMethod(): string; - public function getVersion(): int; + public function getVersion(): string; public function getPackage(): string; diff --git a/src/Handlers/ApiListingHandler.php b/src/Handlers/ApiListingHandler.php index fefc08e..0d0be0e 100644 --- a/src/Handlers/ApiListingHandler.php +++ b/src/Handlers/ApiListingHandler.php @@ -53,7 +53,7 @@ public function handle(array $params): ResponseInterface * * @return array */ - private function getApiList(int $version): array + private function getApiList(string $version): array { $versionApis = array_filter($this->apiDecider->getApis(), function (Api $api) use ($version) { return $version === $api->getEndpoint()->getVersion(); diff --git a/src/Handlers/OpenApiHandler.php b/src/Handlers/OpenApiHandler.php index 60b5a6d..59a6703 100644 --- a/src/Handlers/OpenApiHandler.php +++ b/src/Handlers/OpenApiHandler.php @@ -227,7 +227,7 @@ public function handle(array $params): ResponseInterface return new JsonApiResponse(IResponse::S200_OK, $data); } - private function getApis(int $version): array + private function getApis(string $version): array { return array_filter($this->apiDecider->getApis(), function (Api $api) use ($version) { return $version === $api->getEndpoint()->getVersion(); diff --git a/src/Presenters/ApiPresenter.php b/src/Presenters/ApiPresenter.php index cd06807..ccd0b0b 100644 --- a/src/Presenters/ApiPresenter.php +++ b/src/Presenters/ApiPresenter.php @@ -142,7 +142,7 @@ private function getApi(Request $request): Api { return $this->apiDecider->getApi( $request->getMethod(), - (int) $request->getParameter('version'), + $request->getParameter('version'), $request->getParameter('package'), $request->getParameter('apiAction') ); diff --git a/tests/ApiDeciderTest.php b/tests/ApiDeciderTest.php index 7de42d3..d0c0eb5 100644 --- a/tests/ApiDeciderTest.php +++ b/tests/ApiDeciderTest.php @@ -17,7 +17,7 @@ class ApiDeciderTest extends TestCase public function testDefaultHandlerWithNoRegisteredHandlers() { $apiDecider = new ApiDecider(); - $result = $apiDecider->getApi('POST', 1, 'article', 'list'); + $result = $apiDecider->getApi('POST', '1', 'article', 'list'); $this->assertInstanceOf(EndpointIdentifier::class, $result->getEndpoint()); $this->assertInstanceOf(NoAuthorization::class, $result->getAuthorization()); @@ -28,19 +28,19 @@ public function testFindRightHandler() { $apiDecider = new ApiDecider(); $apiDecider->addApi( - new EndpointIdentifier('POST', 2, 'comments', 'list'), + new EndpointIdentifier('POST', '2', 'comments', 'list'), new AlwaysOkHandler(), new NoAuthorization() ); - $result = $apiDecider->getApi('POST', 2, 'comments', 'list'); + $result = $apiDecider->getApi('POST', '2', 'comments', 'list'); $this->assertInstanceOf(EndpointIdentifier::class, $result->getEndpoint()); $this->assertInstanceOf(NoAuthorization::class, $result->getAuthorization()); $this->assertInstanceOf(AlwaysOkHandler::class, $result->getHandler()); $this->assertEquals('POST', $result->getEndpoint()->getMethod()); - $this->assertEquals(2, $result->getEndpoint()->getVersion()); + $this->assertEquals('2', $result->getEndpoint()->getVersion()); $this->assertEquals('comments', $result->getEndpoint()->getPackage()); $this->assertEquals('list', $result->getEndpoint()->getApiAction()); } @@ -52,7 +52,7 @@ public function testGetHandlers() $this->assertEquals(0, count($apiDecider->getApis())); $apiDecider->addApi( - new EndpointIdentifier('POST', 2, 'comments', 'list'), + new EndpointIdentifier('POST', '2', 'comments', 'list'), new AlwaysOkHandler(), new NoAuthorization() ); @@ -68,14 +68,14 @@ public function testGlobalPreflight() $this->assertEquals(0, count($apiDecider->getApis())); $apiDecider->addApi( - new EndpointIdentifier('POST', 2, 'comments', 'list'), + new EndpointIdentifier('POST', '2', 'comments', 'list'), new AlwaysOkHandler(), new NoAuthorization() ); $this->assertEquals(1, count($apiDecider->getApis())); - $handler = $apiDecider->getApi('OPTIONS', 2, 'comments', 'list'); + $handler = $apiDecider->getApi('OPTIONS', '2', 'comments', 'list'); $this->assertInstanceOf(CorsPreflightHandler::class, $handler->getHandler()); } } diff --git a/tests/EndpoinIdentifierTest.php b/tests/EndpoinIdentifierTest.php index d698ae4..26d5c9f 100644 --- a/tests/EndpoinIdentifierTest.php +++ b/tests/EndpoinIdentifierTest.php @@ -4,6 +4,7 @@ namespace Tomaj\NetteApi\Test\Params; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Tomaj\NetteApi\EndpointIdentifier; @@ -11,19 +12,49 @@ class EndpointIdentifierTest extends TestCase { public function testValidation() { - $endpoint = new EndpointIdentifier('POST', 1, 'core', 'show'); + $endpoint = new EndpointIdentifier('POST', '1', 'core', 'show'); + $this->assertSame('1', $endpoint->getVersion()); $this->assertEquals('POST', $endpoint->getMethod()); - $this->assertEquals(1, $endpoint->getVersion()); $this->assertEquals('core', $endpoint->getPackage()); $this->assertEquals('show', $endpoint->getApiAction()); $this->assertEquals('v1/core/show', $endpoint->getUrl()); + + $endpoint = new EndpointIdentifier('POST', '1.1', 'core', 'show'); + $this->assertEquals('v1.1/core/show', $endpoint->getUrl()); + + $endpoint = new EndpointIdentifier('POST', '1.1.2', 'core', 'show'); + $this->assertEquals('v1.1.2/core/show', $endpoint->getUrl()); } public function testSimpleUrl() { - $endpoint = new EndpointIdentifier('get', 2, 'main', ''); + $endpoint = new EndpointIdentifier('get', '2', 'main', ''); $this->assertNull($endpoint->getApiAction()); $this->assertEquals('GET', $endpoint->getMethod()); } + + public function testSupportedVersions() + { + $endpoint = new EndpointIdentifier('GET', '0', 'core', 'show'); + $this->assertEquals('v0/core/show', $endpoint->getUrl()); + $endpoint = new EndpointIdentifier('GET', '1', 'core', 'show'); + $this->assertEquals('v1/core/show', $endpoint->getUrl()); + $endpoint = new EndpointIdentifier('GET', '1.0', 'core', 'show'); + $this->assertEquals('v1.0/core/show', $endpoint->getUrl()); + $endpoint = new EndpointIdentifier('GET', '1.1', 'core', 'show'); + $this->assertEquals('v1.1/core/show', $endpoint->getUrl()); + $endpoint = new EndpointIdentifier('GET', '1.33', 'core', 'show'); + $this->assertEquals('v1.33/core/show', $endpoint->getUrl()); + $endpoint = new EndpointIdentifier('GET', '1.33-dev', 'core', 'show'); + $this->assertEquals('v1.33-dev/core/show', $endpoint->getUrl()); + $endpoint = new EndpointIdentifier('GET', '0.33.43', 'core', 'show'); + $this->assertEquals('v0.33.43/core/show', $endpoint->getUrl()); + } + + public function testFailVersion() + { + $this->expectException(InvalidArgumentException::class); + new EndpointIdentifier('GET', '1.0/dev', 'core', 'show'); + } } diff --git a/tests/Handler/ApiListingHandlerTest.php b/tests/Handler/ApiListingHandlerTest.php index 791e6f7..76ad650 100644 --- a/tests/Handler/ApiListingHandlerTest.php +++ b/tests/Handler/ApiListingHandlerTest.php @@ -25,18 +25,18 @@ public function testDefaultHandle() $apiDecider = new ApiDecider(); $apiDecider->addApi( - new EndpointIdentifier('POST', 2, 'comments', 'list'), + new EndpointIdentifier('POST', '2', 'comments', 'list'), new AlwaysOkHandler(), new NoAuthorization() ); $apiDecider->addApi( - new EndpointIdentifier('GET', 2, 'endpoints'), + new EndpointIdentifier('GET', '2', 'endpoints'), new ApiListingHandler($apiDecider, $apiLink), new NoAuthorization() ); - $result = $apiDecider->getApi('GET', 2, 'endpoints'); + $result = $apiDecider->getApi('GET', '2', 'endpoints'); $handler = $result->getHandler(); $response = $handler->handle([]); @@ -52,18 +52,18 @@ public function testHandlerWithParam() $apiDecider = new ApiDecider(); $apiDecider->addApi( - new EndpointIdentifier('POST', 1, 'comments', 'list'), + new EndpointIdentifier('POST', '1', 'comments', 'list'), new EchoHandler(), new NoAuthorization() ); $apiDecider->addApi( - new EndpointIdentifier('GET', 1, 'endpoints'), + new EndpointIdentifier('GET', '1', 'endpoints'), new ApiListingHandler($apiDecider, $apiLink), new NoAuthorization() ); - $result = $apiDecider->getApi('GET', 1, 'endpoints'); + $result = $apiDecider->getApi('GET', '1', 'endpoints'); $handler = $result->getHandler(); $response = $handler->handle([]); diff --git a/tests/Handler/CorsPreflightHandlerTest.php b/tests/Handler/CorsPreflightHandlerTest.php index eacd8ca..fea17da 100644 --- a/tests/Handler/CorsPreflightHandlerTest.php +++ b/tests/Handler/CorsPreflightHandlerTest.php @@ -28,7 +28,7 @@ public function testEndpointSetter() $defaultHandler = new CorsPreflightHandler(new Response()); $this->assertNull($defaultHandler->getEndpoint()); - $endpointIdentifier = new EndpointIdentifier('OPTIONS', 1, 'article', 'detail'); + $endpointIdentifier = new EndpointIdentifier('OPTIONS', '1', 'article', 'detail'); $defaultHandler->setEndpointIdentifier($endpointIdentifier); $this->assertEquals($endpointIdentifier, $defaultHandler->getEndpoint()); } @@ -61,7 +61,7 @@ public function testCreateLink() $linkGenerator = new LinkGenerator(new SimpleRouter([]), new UrlScript('http://test/')); $defaultHandler->setupLinkGenerator($linkGenerator); - $endpointIdentifier = new EndpointIdentifier('OPTIONS', 1, 'article', 'detail'); + $endpointIdentifier = new EndpointIdentifier('OPTIONS', '1', 'article', 'detail'); $defaultHandler->setEndpointIdentifier($endpointIdentifier); $this->assertEquals('http://test/?version=1&package=article&apiAction=detail&page=2&action=default&presenter=Api%3AApi', $defaultHandler->createLink(['page' => 2])); diff --git a/tests/Handler/DefaultHandlerTest.php b/tests/Handler/DefaultHandlerTest.php index e67f8f6..00e0160 100644 --- a/tests/Handler/DefaultHandlerTest.php +++ b/tests/Handler/DefaultHandlerTest.php @@ -29,7 +29,7 @@ public function testEndpointSetter() $defaultHandler = new DefaultHandler(); $this->assertNull($defaultHandler->getEndpoint()); - $endpointIdentifier = new EndpointIdentifier('POST', 1, 'article', 'detail'); + $endpointIdentifier = new EndpointIdentifier('POST', '1', 'article', 'detail'); $defaultHandler->setEndpointIdentifier($endpointIdentifier); $this->assertEquals($endpointIdentifier, $defaultHandler->getEndpoint()); } @@ -62,7 +62,7 @@ public function testCreateLink() $linkGenerator = new LinkGenerator(new SimpleRouter([]), new UrlScript('http://test/')); $defaultHandler->setupLinkGenerator($linkGenerator); - $endpointIdentifier = new EndpointIdentifier('POST', 1, 'article', 'detail'); + $endpointIdentifier = new EndpointIdentifier('POST', '1', 'article', 'detail'); $defaultHandler->setEndpointIdentifier($endpointIdentifier); $this->assertEquals('http://test/?version=1&package=article&apiAction=detail&page=2&action=default&presenter=Api%3AApi', $defaultHandler->createLink(['page' => 2])); diff --git a/tests/Handler/OpenApiHandlerTest.php b/tests/Handler/OpenApiHandlerTest.php index ccb9d0e..0ee588d 100644 --- a/tests/Handler/OpenApiHandlerTest.php +++ b/tests/Handler/OpenApiHandlerTest.php @@ -25,18 +25,18 @@ public function testHandlerWithMultipleResponseSchemas() $apiDecider = new ApiDecider(); $apiDecider->addApi( - new EndpointIdentifier('GET', 1, 'test'), + new EndpointIdentifier('GET', '1', 'test'), new MultipleOutputTestHandler(), new NoAuthorization() ); $apiDecider->addApi( - new EndpointIdentifier('GET', 1, 'docs', 'open-api'), + new EndpointIdentifier('GET', '1', 'docs', 'open-api'), new OpenApiHandler($apiDecider, $apiLink, $request), new NoAuthorization() ); - $result = $apiDecider->getApi('GET', 1, 'docs', 'open-api'); + $result = $apiDecider->getApi('GET', '1', 'docs', 'open-api'); $handler = $result->getHandler(); $response = $handler->handle(['format' => 'json']); diff --git a/tests/Presenters/ApiPresenterTest.php b/tests/Presenters/ApiPresenterTest.php index ef1dea1..58766a8 100644 --- a/tests/Presenters/ApiPresenterTest.php +++ b/tests/Presenters/ApiPresenterTest.php @@ -27,14 +27,14 @@ class ApiPresenterTest extends TestCase public function testSimpleResponse() { $apiDecider = new ApiDecider(); - $apiDecider->addApi(new EndpointIdentifier('GET', 1, 'test', 'api'), new AlwaysOkHandler(), new NoAuthorization()); + $apiDecider->addApi(new EndpointIdentifier('GET', '1', 'test', 'api'), new AlwaysOkHandler(), new NoAuthorization()); $presenter = new ApiPresenter(); $presenter->apiDecider = $apiDecider; $presenter->response = new HttpResponse(); $presenter->context = new Container(); - $request = new Request('Api:Api:default', 'GET', ['version' => 1, 'package' => 'test', 'apiAction' => 'api']); + $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); $this->assertEquals(200, $result->getCode()); @@ -47,7 +47,7 @@ public function testWithAuthorization() { $apiDecider = new ApiDecider(); $apiDecider->addApi( - new EndpointIdentifier('GET', 1, 'test', 'api'), + new EndpointIdentifier('GET', '1', 'test', 'api'), new AlwaysOkHandler(), new BearerTokenAuthorization(new StaticTokenRepository([]), new IpDetector()) ); @@ -57,7 +57,7 @@ public function testWithAuthorization() $presenter->response = new HttpResponse(); $presenter->context = new Container(); - $request = new Request('Api:Api:default', 'GET', ['version' => 1, 'package' => 'test', 'apiAction' => 'api']); + $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); $this->assertEquals(['status' => 'error', 'message' => 'Authorization header HTTP_Authorization is not set'], $result->getPayload()); @@ -68,7 +68,7 @@ public function testWithParams() { $apiDecider = new ApiDecider(); $apiDecider->addApi( - new EndpointIdentifier('GET', 1, 'test', 'api'), + new EndpointIdentifier('GET', '1', 'test', 'api'), new EchoHandler(), new NoAuthorization() ); @@ -80,7 +80,7 @@ public function testWithParams() Debugger::$productionMode = Debugger::PRODUCTION; - $request = new Request('Api:Api:default', 'GET', ['version' => 1, 'package' => 'test', 'apiAction' => 'api']); + $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); $this->assertEquals(['status' => 'error', 'message' => 'wrong input'], $result->getPayload()); @@ -97,7 +97,7 @@ public function testWithOutputs() { $apiDecider = new ApiDecider(); $apiDecider->addApi( - new EndpointIdentifier('GET', 1, 'test', 'api'), + new EndpointIdentifier('GET', '1', 'test', 'api'), new TestHandler(), new NoAuthorization() ); @@ -107,7 +107,7 @@ public function testWithOutputs() $presenter->response = new HttpResponse(); $presenter->context = new Container(); - $request = new Request('Api:Api:default', 'GET', ['version' => 1, 'package' => 'test', 'apiAction' => 'api']); + $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); $this->assertEquals(['hello' => 'world'], $result->getPayload()); From 5347a7129679af7027a7f98a16ab583b57297afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ber=C3=A1nek?= Date: Fri, 19 Apr 2024 20:44:13 +0200 Subject: [PATCH 04/12] Refactor JsonOutput and OpenApiHandler classes to support multiple examples (#143) Co-authored-by: Martin Beranek --- src/Handlers/OpenApiHandler.php | 29 +++++++++++++++++--- src/Output/AbstractOutput.php | 47 +++++++++++++++++++++++++++++++++ src/Params/InputParam.php | 35 +++++++++++++++++++++--- src/Params/JsonInputParam.php | 40 +++++++++++++++++++++++++++- 4 files changed, 142 insertions(+), 9 deletions(-) diff --git a/src/Handlers/OpenApiHandler.php b/src/Handlers/OpenApiHandler.php index 59a6703..7f372c4 100644 --- a/src/Handlers/OpenApiHandler.php +++ b/src/Handlers/OpenApiHandler.php @@ -313,6 +313,17 @@ private function getPaths(array $versionApis, string $baseUrl, string $basePath) ], ] ]; + if (!empty($examples = $output->getExamples())) { + if (count($examples) === 1) { + $example = is_array($output->getExample())? $output->getExample() : json_decode($output->getExample(), true); + $responses[$output->getCode()]['content']['application/json; charset=utf-8']['example'] = $example; + } else { + foreach ($examples as $exampleKey => $example) { + $example = is_array($example)? $example : json_decode($example, true); + $responses[$output->getCode()]['content']['application/json; charset=utf-8']['examples'][$exampleKey] = $example; + } + } + } } else { if (!isset($responses[$output->getCode()]['content']['application/json; charset=utf-8']['schema']['oneOf'])) { $tmp = $responses[$output->getCode()]['content']['application/json; charset=utf-8']['schema']; @@ -476,8 +487,14 @@ private function createRequestBody(ApiHandlerInterface $handler) foreach ($handler->params() as $param) { if ($param instanceof JsonInputParam) { $schema = json_decode($param->getSchema(), true); - if ($param->getExample()) { - $schema['example'] = $param->getExample(); + if (!empty($examples = $param->getExamples())) { + if (count($examples) === 1) { + $schema['example'] = is_array($param->getExample())? $param->getExample() : json_decode($param->getExample(), true); + } else { + foreach ($examples as $exampleKey => $example) { + $schema['examples'][$exampleKey] = is_array($example)? $example : json_decode($example, true); + } + } } return [ 'description' => $param->getDescription(), @@ -493,8 +510,12 @@ private function createRequestBody(ApiHandlerInterface $handler) $schema = [ 'type' => 'string', ]; - if ($param->getExample()) { - $schema['example'] = $param->getExample(); + if (!empty($examples = $param->getExamples())) { + if (count($examples) === 1) { + $schema['example'] = $param->getExample(); + } else { + $schema['examples'] = $examples; + } } return [ 'description' => $param->getDescription(), diff --git a/src/Output/AbstractOutput.php b/src/Output/AbstractOutput.php index c36f779..ed2a0f2 100644 --- a/src/Output/AbstractOutput.php +++ b/src/Output/AbstractOutput.php @@ -10,6 +10,9 @@ abstract class AbstractOutput implements OutputInterface protected $description; + /** @var array */ + protected $examples = []; + public function __construct(int $code, string $description = '') { $this->code = $code; @@ -25,4 +28,48 @@ public function getDescription(): string { return $this->description; } + + /** + * @param string $name Example name + * @param mixed $example Example + * @return Self + */ + public function addExample(string $name, $example): self + { + $this->examples[$name] = $example; + return $this; + } + + /** + * Set default example + * @param mixed $example + * @return self + * @deprecated Use addExample instead + */ + public function setExample($example): self + { + $this->examples["default"] = $example; + return $this; + } + + /** + * Returns first example + * @return mixed + */ + public function getExample() + { + if (empty($this->examples)) { + return null; + } + return reset($this->examples); + } + + /** + * Returns all examples + * @return array + */ + public function getExamples(): array + { + return $this->examples; + } } diff --git a/src/Params/InputParam.php b/src/Params/InputParam.php index 6cd8f02..468520f 100644 --- a/src/Params/InputParam.php +++ b/src/Params/InputParam.php @@ -44,8 +44,8 @@ abstract class InputParam implements ParamInterface /** @var mixed */ protected $default; - /** @var mixed */ - protected $example; + /** @var array */ + protected $examples = []; public function __construct(string $key) { @@ -128,21 +128,48 @@ public function getDefault() } /** + * Add example, can be used multiple times to add many examples + * @param string $name Example name + * @param mixed $example Example + * @return Self + */ + public function addExample(string $name, $example): self + { + $this->examples[$name] = $example; + return $this; + } + + /** + * Set default example * @param mixed $example * @return self + * @deprecated Use addExample instead */ public function setExample($example): self { - $this->example = $example; + $this->examples["default"] = $example; return $this; } /** + * Returns first example * @return mixed */ public function getExample() { - return $this->example; + if (empty($this->examples)) { + return null; + } + return reset($this->examples); + } + + /** + * Returns all examples + * @return array + */ + public function getExamples(): array + { + return $this->examples; } public function updateConsoleForm(Form $form): void diff --git a/src/Params/JsonInputParam.php b/src/Params/JsonInputParam.php index 6b1e3f1..f7ab440 100644 --- a/src/Params/JsonInputParam.php +++ b/src/Params/JsonInputParam.php @@ -66,10 +66,48 @@ public function getSchema(): string protected function addFormInput(Form $form, string $key): BaseControl { + $fullSchema = json_decode($this->schema, true); + + if (!empty($examples = $this->getExamples())) { + if (count($examples) === 1) { + $fullSchema['example'] = is_array($this->getExample())? $this->getExample() : json_decode($this->getExample(), true); + } else { + foreach ($examples as $exampleKey => $example) { + $fullSchema['examples'][$exampleKey] = is_array($example)? $example : json_decode($example, true); + // pretty formatting of json example if decoded + } + } + } + + + if (!empty($fullSchema['examples'])) { + $this->description .= <<< HTML +
+ Select Example:  +HTML; + foreach ($fullSchema['examples'] as $exampleKey => $exampleValue) { + $example = htmlentities(json_encode($exampleValue, JSON_PRETTY_PRINT)); + $this->description .= <<< HTML +
+ {$exampleKey} +
+HTML; + } + $this->description .= <<< HTML + +
+ +HTML; + } $this->description .= ' '; + . nl2br(str_replace(' ', ' ', json_encode($fullSchema, JSON_PRETTY_PRINT))) . ''; return $form->addTextArea('post_raw', $this->getParamLabel()) ->setHtmlAttribute('rows', 10); From 7d99bb7166f76e0e7f33727811ac7ec1e1921d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ber=C3=A1nek?= Date: Mon, 13 May 2024 19:17:17 +0200 Subject: [PATCH 05/12] EBOX-1696-Do-not-use-schema-validate-on-production-just-for-development (#147) Co-authored-by: Martin Beranek --- src/Presenters/ApiPresenter.php | 35 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Presenters/ApiPresenter.php b/src/Presenters/ApiPresenter.php index ccd0b0b..b3190f5 100644 --- a/src/Presenters/ApiPresenter.php +++ b/src/Presenters/ApiPresenter.php @@ -91,30 +91,29 @@ public function run(Request $request): IResponse return $response; } $params = $paramsProcessor->getValues(); - try { $response = $handler->handle($params); - $outputValid = count($handler->outputs()) === 0; // back compatibility for handlers with no outputs defined - $outputValidatorErrors = []; - foreach ($handler->outputs() as $output) { - if (!$output instanceof OutputInterface) { - $outputValidatorErrors[] = ["Output does not implement OutputInterface"]; - continue; - } - $validationResult = $output->validate($response); - if ($validationResult->isOk()) { - $outputValid = true; - break; + $code = $response->getCode(); + if (!Debugger::$productionMode) { /// If not production mode, validate output + $outputValid = count($handler->outputs()) === 0; // back compatibility for handlers with no outputs defined + $outputValidatorErrors = []; + foreach ($handler->outputs() as $output) { + if (!$output instanceof OutputInterface) { + $outputValidatorErrors[] = ["Output does not implement OutputInterface"]; + continue; + } + $validationResult = $output->validate($response); + if ($validationResult->isOk()) { + $outputValid = true; + break; + } + $outputValidatorErrors[] = $validationResult->getErrors(); } - $outputValidatorErrors[] = $validationResult->getErrors(); - } - if (!$outputValid) { - Debugger::log($outputValidatorErrors, Debugger::ERROR); - if (!Debugger::$productionMode) { + if (!$outputValid) { + Debugger::log($outputValidatorErrors, Debugger::ERROR); $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'details' => $outputValidatorErrors]); } } - $code = $response->getCode(); } catch (Throwable $exception) { if (!Debugger::$productionMode) { $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $exception->getMessage()]); From 69e2be31b34667f07c345fe1d2e598d3fc91bf82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ber=C3=A1nek?= Date: Tue, 14 May 2024 10:00:10 +0200 Subject: [PATCH 06/12] Allow int in endpoint identifier version (#148) Co-authored-by: Martin Beranek --- CHANGELOG.md | 2 +- src/EndpointIdentifier.php | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2cdc4a..3eb7cf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [Unreleased][unreleased] #### Changed -* [BC] Support for semantic versioning api. Need retype version from `int` to `string`. +* Support for semantic versioning api. * [BC] DefaultHandler response code 404 instead 400 #### Added diff --git a/src/EndpointIdentifier.php b/src/EndpointIdentifier.php index e91a102..43fae40 100644 --- a/src/EndpointIdentifier.php +++ b/src/EndpointIdentifier.php @@ -16,8 +16,15 @@ class EndpointIdentifier implements EndpointInterface private $apiAction; - public function __construct(string $method, string $version, string $package, ?string $apiAction = null) + /** + * @param string $method example: "GET", "POST", "PUT", "DELETE" + * @param string|int $version Version must have semantic numbering. For example "1", "1.1", "0.13.2" etc. + * @param string $package example: "users" + * @param string|null $apiAction example: "query" + */ + public function __construct(string $method, $version, string $package, ?string $apiAction = null) { + $version = (string) $version; $this->method = strtoupper($method); if (strpos($version, '/') !== false) { throw new InvalidArgumentException('Version must have semantic numbering. For example "1", "1.1", "0.13.2" etc.'); From 1927ee9dfdb04ba6aee32cc36aa8f7813485e1c0 Mon Sep 17 00:00:00 2001 From: Michal Lulco Date: Tue, 23 Jul 2024 12:00:01 +0200 Subject: [PATCH 07/12] Lazy api handlers (#149) --- CHANGELOG.md | 2 ++ README.md | 24 +++++++++++---- src/Api.php | 13 +++++++-- src/ApiDecider.php | 39 +++++++++++++++++++++---- tests/ApiDeciderTest.php | 18 +++++++++--- tests/Handler/ApiListingHandlerTest.php | 13 +++++++-- tests/Handler/OpenApiHandlerTest.php | 11 ++++++- tests/Presenters/ApiPresenterTest.php | 16 +++++++--- 8 files changed, 113 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eb7cf3..67dc7df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip #### Changed * Support for semantic versioning api. * [BC] DefaultHandler response code 404 instead 400 +* [BC] Added Container to API Decider #### Added * CorsPreflightHandlerInterface - resolve multiple service registered handler error +* Lazy API handlers #### Added * Button to copy `Body` content in api console diff --git a/README.md b/README.md index 786f1bc..7f321ba 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,26 @@ After that you need only register your API handlers to *apiDecider* [ApiDecider] ```neon services: - - Tomaj\NetteApi\Link\ApiLink - - Tomaj\NetteApi\Misc\IpDetector - apiDecider: + - Tomaj\NetteApi\Link\ApiLink + - Tomaj\NetteApi\Misc\IpDetector + apiDecider: + factory: Tomaj\NetteApi\ApiDecider + setup: + - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\NoAuthorization()) + - addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email'), \App\MyApi\v1\Handlers\SendEmailHandler(), \Tomaj\NetteApi\Authorization\BearerTokenAuthorization()) + +``` + +or lazy (preferred because of performance) +```neon +services: + - App\MyApi\v1\Handlers\SendEmailLazyHandler() + sendEmailLazyNamed: App\MyApi\v1\Handlers\SendEmailLazyNamedHandler() + factory: Tomaj\NetteApi\ApiDecider setup: - - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\NoAuthorization()) - - addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email'), \App\MyApi\v1\Handlers\SendEmailHandler(), \Tomaj\NetteApi\Authorization\BearerTokenAuthorization()) + - addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email-lazy'), 'App\MyApi\v1\Handlers\SendEmailHandler', \Tomaj\NetteApi\Authorization\BearerTokenAuthorization()) + - addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email-lazy-named'), '@sendEmailLazyNamed', \Tomaj\NetteApi\Authorization\BearerTokenAuthorization()) ``` As you can see in example, you can register as many endpoints as you want with different configurations. Nette-Api supports API versioning from the beginning. @@ -329,6 +342,7 @@ services: - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\BasicBasicAuthentication(['first-user': 'first-password', 'second-user': 'second-password'])) ``` + ### Bearer token authentication For simple use of Bearer token authorization with few tokens, you can use [StaticTokenRepository](src/Misc/StaticTokenRepository.php) (Tomaj\NetteApi\Misc\StaticTokenRepository). diff --git a/src/Api.php b/src/Api.php index 0e33705..5c53491 100644 --- a/src/Api.php +++ b/src/Api.php @@ -19,9 +19,15 @@ class Api private $rateLimit; + /** + * @param EndpointInterface $endpoint + * @param ApiHandlerInterface|string $handler + * @param ApiAuthorizationInterface $authorization + * @param RateLimitInterface|null $rateLimit + */ public function __construct( EndpointInterface $endpoint, - ApiHandlerInterface $handler, + $handler, ApiAuthorizationInterface $authorization, ?RateLimitInterface $rateLimit = null ) { @@ -36,7 +42,10 @@ public function getEndpoint(): EndpointInterface return $this->endpoint; } - public function getHandler(): ApiHandlerInterface + /** + * @return ApiHandlerInterface|string + */ + public function getHandler() { return $this->handler; } diff --git a/src/ApiDecider.php b/src/ApiDecider.php index feadf96..541bd2e 100644 --- a/src/ApiDecider.php +++ b/src/ApiDecider.php @@ -4,6 +4,7 @@ namespace Tomaj\NetteApi; +use Nette\DI\Container; use Nette\Http\Response; use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface; use Tomaj\NetteApi\Authorization\NoAuthorization; @@ -15,12 +16,20 @@ class ApiDecider { + /** @var Container */ + private $container; + /** @var Api[] */ private $apis = []; /** @var ApiHandlerInterface|null */ private $globalPreflightHandler = null; + public function __construct(Container $container) + { + $this->container = $container; + } + /** * Get api handler that match input method, version, package and apiAction. * If decider cannot find handler for given handler, returns defaults. @@ -41,8 +50,9 @@ public function getApi(string $method, string $version, string $package, ?string $identifier = $api->getEndpoint(); if ($method === $identifier->getMethod() && $identifier->getVersion() === $version && $identifier->getPackage() === $package && $identifier->getApiAction() === $apiAction) { $endpointIdentifier = new EndpointIdentifier($method, $version, $package, $apiAction); - $api->getHandler()->setEndpointIdentifier($endpointIdentifier); - return $api; + $handler = $this->getHandler($api); + $handler->setEndpointIdentifier($endpointIdentifier); + return new Api($api->getEndpoint(), $handler, $api->getAuthorization(), $api->getRateLimit()); } if ($method === 'OPTIONS' && $this->globalPreflightHandler && $identifier->getVersion() === $version && $identifier->getPackage() === $package && $identifier->getApiAction() === $apiAction) { return new Api(new EndpointIdentifier('OPTIONS', $version, $package, $apiAction), $this->globalPreflightHandler, new NoAuthorization()); @@ -63,12 +73,12 @@ public function enableGlobalPreflight(CorsPreflightHandlerInterface $corsHandler * Register new api handler * * @param EndpointInterface $endpointIdentifier - * @param ApiHandlerInterface $handler + * @param ApiHandlerInterface|string $handler * @param ApiAuthorizationInterface $apiAuthorization * @param RateLimitInterface|null $rateLimit * @return self */ - public function addApi(EndpointInterface $endpointIdentifier, ApiHandlerInterface $handler, ApiAuthorizationInterface $apiAuthorization, RateLimitInterface $rateLimit = null): self + public function addApi(EndpointInterface $endpointIdentifier, $handler, ApiAuthorizationInterface $apiAuthorization, RateLimitInterface $rateLimit = null): self { $this->apis[] = new Api($endpointIdentifier, $handler, $apiAuthorization, $rateLimit); return $this; @@ -81,6 +91,25 @@ public function addApi(EndpointInterface $endpointIdentifier, ApiHandlerInterfac */ public function getApis(): array { - return $this->apis; + $apis = []; + foreach ($this->apis as $api) { + $handler = $this->getHandler($api); + $apis[] = new Api($api->getEndpoint(), $handler, $api->getAuthorization(), $api->getRateLimit()); + } + return $apis; + } + + private function getHandler(Api $api): ApiHandlerInterface + { + $handler = $api->getHandler(); + if (!is_string($handler)) { + return $handler; + } + + if (str_starts_with($handler, '@')) { + return $this->container->getByName(substr($handler, 1)); + } + + return $this->container->getByType($handler); } } diff --git a/tests/ApiDeciderTest.php b/tests/ApiDeciderTest.php index d0c0eb5..ab87135 100644 --- a/tests/ApiDeciderTest.php +++ b/tests/ApiDeciderTest.php @@ -4,6 +4,7 @@ namespace Tomaj\NetteApi\Test\Params; +use Nette\DI\Container; use PHPUnit\Framework\TestCase; use Tomaj\NetteApi\ApiDecider; use Tomaj\NetteApi\Authorization\NoAuthorization; @@ -14,9 +15,18 @@ class ApiDeciderTest extends TestCase { + /** @var Container */ + private $container; + + protected function setUp(): void + { + $this->container = new Container(); + } + public function testDefaultHandlerWithNoRegisteredHandlers() { - $apiDecider = new ApiDecider(); + + $apiDecider = new ApiDecider($this->container); $result = $apiDecider->getApi('POST', '1', 'article', 'list'); $this->assertInstanceOf(EndpointIdentifier::class, $result->getEndpoint()); @@ -26,7 +36,7 @@ public function testDefaultHandlerWithNoRegisteredHandlers() public function testFindRightHandler() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( new EndpointIdentifier('POST', '2', 'comments', 'list'), new AlwaysOkHandler(), @@ -47,7 +57,7 @@ public function testFindRightHandler() public function testGetHandlers() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $this->assertEquals(0, count($apiDecider->getApis())); @@ -62,7 +72,7 @@ public function testGetHandlers() public function testGlobalPreflight() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->enableGlobalPreflight(); $this->assertEquals(0, count($apiDecider->getApis())); diff --git a/tests/Handler/ApiListingHandlerTest.php b/tests/Handler/ApiListingHandlerTest.php index 76ad650..fc3183d 100644 --- a/tests/Handler/ApiListingHandlerTest.php +++ b/tests/Handler/ApiListingHandlerTest.php @@ -6,6 +6,7 @@ use Nette\Application\LinkGenerator; use Nette\Application\Routers\SimpleRouter; +use Nette\DI\Container; use Nette\Http\UrlScript; use PHPUnit\Framework\TestCase; use Tomaj\NetteApi\ApiDecider; @@ -18,12 +19,20 @@ class ApiListingHandlerTest extends TestCase { + /** @var Container */ + private $container; + + protected function setUp(): void + { + $this->container = new Container(); + } + public function testDefaultHandle() { $linkGenerator = new LinkGenerator(new SimpleRouter([]), new UrlScript('http://test/')); $apiLink = new ApiLink($linkGenerator); - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( new EndpointIdentifier('POST', '2', 'comments', 'list'), new AlwaysOkHandler(), @@ -50,7 +59,7 @@ public function testHandlerWithParam() $linkGenerator = new LinkGenerator(new SimpleRouter([]), new UrlScript('http://test/')); $apiLink = new ApiLink($linkGenerator); - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( new EndpointIdentifier('POST', '1', 'comments', 'list'), new EchoHandler(), diff --git a/tests/Handler/OpenApiHandlerTest.php b/tests/Handler/OpenApiHandlerTest.php index 0ee588d..5667173 100644 --- a/tests/Handler/OpenApiHandlerTest.php +++ b/tests/Handler/OpenApiHandlerTest.php @@ -6,6 +6,7 @@ use Nette\Application\LinkGenerator; use Nette\Application\Routers\SimpleRouter; +use Nette\DI\Container; use Nette\Http\UrlScript; use PHPUnit\Framework\TestCase; use Tomaj\NetteApi\ApiDecider; @@ -17,13 +18,21 @@ class OpenApiHandlerTest extends TestCase { + /** @var Container */ + private $container; + + protected function setUp(): void + { + $this->container = new Container(); + } + public function testHandlerWithMultipleResponseSchemas() { $linkGenerator = new LinkGenerator(new SimpleRouter([]), new UrlScript('http://test/')); $apiLink = new ApiLink($linkGenerator); $request = new Request(new UrlScript('http://test/')); - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( new EndpointIdentifier('GET', '1', 'test'), new MultipleOutputTestHandler(), diff --git a/tests/Presenters/ApiPresenterTest.php b/tests/Presenters/ApiPresenterTest.php index 58766a8..5eca87b 100644 --- a/tests/Presenters/ApiPresenterTest.php +++ b/tests/Presenters/ApiPresenterTest.php @@ -24,9 +24,17 @@ class ApiPresenterTest extends TestCase { + /** @var Container */ + private $container; + + protected function setUp(): void + { + $this->container = new Container(); + } + public function testSimpleResponse() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi(new EndpointIdentifier('GET', '1', 'test', 'api'), new AlwaysOkHandler(), new NoAuthorization()); $presenter = new ApiPresenter(); @@ -45,7 +53,7 @@ public function testSimpleResponse() public function testWithAuthorization() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( new EndpointIdentifier('GET', '1', 'test', 'api'), new AlwaysOkHandler(), @@ -66,7 +74,7 @@ public function testWithAuthorization() public function testWithParams() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( new EndpointIdentifier('GET', '1', 'test', 'api'), new EchoHandler(), @@ -95,7 +103,7 @@ public function testWithParams() public function testWithOutputs() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( new EndpointIdentifier('GET', '1', 'test', 'api'), new TestHandler(), From 90dea23d61c7736e0ab7d3c3f3d0382a525d994c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ber=C3=A1nek?= Date: Tue, 13 Aug 2024 21:24:03 +0200 Subject: [PATCH 08/12] Schema validation and error detail settings by get params (#150) Co-authored-by: Martin Beranek --- CHANGELOG.md | 2 + README.md | 7 ++++ .../Configurator/ConfiguratorInterface.php | 14 +++++++ .../Configurator/DebuggerConfigurator.php | 21 ++++++++++ src/Output/Configurator/EnvConfigurator.php | 41 +++++++++++++++++++ src/Output/Configurator/QueryConfigurator.php | 41 +++++++++++++++++++ src/Presenters/ApiPresenter.php | 11 +++-- tests/Presenters/ApiPresenterTest.php | 9 ++-- 8 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 src/Output/Configurator/ConfiguratorInterface.php create mode 100644 src/Output/Configurator/DebuggerConfigurator.php create mode 100644 src/Output/Configurator/EnvConfigurator.php create mode 100644 src/Output/Configurator/QueryConfigurator.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 67dc7df..f0ae678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip * Support for semantic versioning api. * [BC] DefaultHandler response code 404 instead 400 * [BC] Added Container to API Decider +* [BC] Output Configurator, Allows different methods for output configuration. Needs to be added to config services. #### Added * CorsPreflightHandlerInterface - resolve multiple service registered handler error @@ -14,6 +15,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip #### Added * Button to copy `Body` content in api console +* Ability to disable schema validation and provide additional error info with get parameters. #### Changed * Handler tag wrapper has changed class from `btn` to `label` diff --git a/README.md b/README.md index 7f321ba..f164740 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,13 @@ application: Api: Tomaj\NetteApi\Presenters\*Presenter ``` +Then register your preffered output configurator in *config.neon* services: + +```neon +services: + apiOutputConfigurator: Tomaj\NetteApi\Output\Configurator\DebuggerConfigurator +``` + And add route to you RouterFactory: ```php diff --git a/src/Output/Configurator/ConfiguratorInterface.php b/src/Output/Configurator/ConfiguratorInterface.php new file mode 100644 index 0000000..01c817e --- /dev/null +++ b/src/Output/Configurator/ConfiguratorInterface.php @@ -0,0 +1,14 @@ +envVariable = $envVariable; + $this->productionValue = $productionValue; + } + + public function validateSchema(?Request $request = null): bool + { + $appEnv = getenv($this->envVariable); + if ($appEnv === $this->productionValue) { + return false; + } + return true; + } + + public function showErrorDetail(?Request $request = null): bool + { + $appEnv = getenv($this->envVariable); + if ($appEnv === $this->productionValue) { + return false; + } + return true; + } +} diff --git a/src/Output/Configurator/QueryConfigurator.php b/src/Output/Configurator/QueryConfigurator.php new file mode 100644 index 0000000..97efcf4 --- /dev/null +++ b/src/Output/Configurator/QueryConfigurator.php @@ -0,0 +1,41 @@ +noSchemaValidateParam = $noSchemaValidateParam; + $this->errorDetailParam = $errorDetailParam; + } + + public function validateSchema(?Request $request = null): bool + { + if ($request === null) { + return false; + } + $getParams = $request->getParameters(); + return !isset($getParams[$this->noSchemaValidateParam]); + } + + public function showErrorDetail(?Request $request = null): bool + { + if ($request === null) { + return false; + } + $getParams = $request->getParameters(); + return isset($getParams[$this->errorDetailParam]); + } +} diff --git a/src/Presenters/ApiPresenter.php b/src/Presenters/ApiPresenter.php index b3190f5..797611e 100644 --- a/src/Presenters/ApiPresenter.php +++ b/src/Presenters/ApiPresenter.php @@ -21,6 +21,7 @@ use Tomaj\NetteApi\RateLimit\RateLimitInterface; use Tomaj\NetteApi\Response\JsonApiResponse; use Tracy\Debugger; +use Tomaj\NetteApi\Output\Configurator\ConfiguratorInterface; final class ApiPresenter implements IPresenter { @@ -33,6 +34,9 @@ final class ApiPresenter implements IPresenter /** @var Container @inject */ public $context; + /** @var ConfiguratorInterface @inject */ + public $outputConfigurator; + /** * CORS header settings * @@ -83,7 +87,7 @@ public function run(Request $request): IResponse $paramsProcessor = new ParamsProcessor($handler->params()); if ($paramsProcessor->isError()) { $this->response->setCode(Response::S400_BAD_REQUEST); - if (!Debugger::$productionMode) { + if ($this->outputConfigurator->showErrorDetail($request)) { $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input', 'detail' => $paramsProcessor->getErrors()]); } else { $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input']); @@ -94,7 +98,8 @@ public function run(Request $request): IResponse try { $response = $handler->handle($params); $code = $response->getCode(); - if (!Debugger::$productionMode) { /// If not production mode, validate output + + if ($this->outputConfigurator->validateSchema($request)) { $outputValid = count($handler->outputs()) === 0; // back compatibility for handlers with no outputs defined $outputValidatorErrors = []; foreach ($handler->outputs() as $output) { @@ -115,7 +120,7 @@ public function run(Request $request): IResponse } } } catch (Throwable $exception) { - if (!Debugger::$productionMode) { + if ($this->outputConfigurator->showErrorDetail($request)) { $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $exception->getMessage()]); } else { $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']); diff --git a/tests/Presenters/ApiPresenterTest.php b/tests/Presenters/ApiPresenterTest.php index 5eca87b..804ace4 100644 --- a/tests/Presenters/ApiPresenterTest.php +++ b/tests/Presenters/ApiPresenterTest.php @@ -4,12 +4,10 @@ namespace Tomaj\NetteApi\Test\Presenters; -use PHPUnit\Framework\TestCase; use Nette\Application\Request; use Nette\DI\Container; use Nette\Http\Response as HttpResponse; -use Nette\Http\Request as HttpRequest; -use Nette\Http\UrlScript; +use PHPUnit\Framework\TestCase; use Tomaj\NetteApi\ApiDecider; use Tomaj\NetteApi\Authorization\BearerTokenAuthorization; use Tomaj\NetteApi\Authorization\NoAuthorization; @@ -18,6 +16,7 @@ use Tomaj\NetteApi\Handlers\EchoHandler; use Tomaj\NetteApi\Misc\IpDetector; use Tomaj\NetteApi\Misc\StaticTokenRepository; +use Tomaj\NetteApi\Output\Configurator\DebuggerConfigurator; use Tomaj\NetteApi\Presenters\ApiPresenter; use Tomaj\NetteApi\Test\Handler\TestHandler; use Tracy\Debugger; @@ -41,6 +40,7 @@ public function testSimpleResponse() $presenter->apiDecider = $apiDecider; $presenter->response = new HttpResponse(); $presenter->context = new Container(); + $presenter->outputConfigurator = new DebuggerConfigurator(); $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); @@ -64,6 +64,7 @@ public function testWithAuthorization() $presenter->apiDecider = $apiDecider; $presenter->response = new HttpResponse(); $presenter->context = new Container(); + $presenter->outputConfigurator = new DebuggerConfigurator(); $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); @@ -85,6 +86,7 @@ public function testWithParams() $presenter->apiDecider = $apiDecider; $presenter->response = new HttpResponse(); $presenter->context = new Container(); + $presenter->outputConfigurator = new DebuggerConfigurator(); Debugger::$productionMode = Debugger::PRODUCTION; @@ -114,6 +116,7 @@ public function testWithOutputs() $presenter->apiDecider = $apiDecider; $presenter->response = new HttpResponse(); $presenter->context = new Container(); + $presenter->outputConfigurator = new DebuggerConfigurator(); $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); From b703eafec79efdcd4570ce8a06dda706a2097e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ber=C3=A1nek?= Date: Thu, 15 Aug 2024 16:01:54 +0200 Subject: [PATCH 09/12] Beranek/schema validation and error detail settings by get params (#151) Co-authored-by: Martin Beranek --- CHANGELOG.md | 1 + .../Configurator/ConfiguratorInterface.php | 6 ++-- .../Configurator/DebuggerConfigurator.php | 5 ++- src/Output/Configurator/EnvConfigurator.php | 6 ++-- src/Output/Configurator/QueryConfigurator.php | 35 +++++++++---------- src/Presenters/ApiPresenter.php | 6 ++-- 6 files changed, 27 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0ae678..b53b404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip * [BC] DefaultHandler response code 404 instead 400 * [BC] Added Container to API Decider * [BC] Output Configurator, Allows different methods for output configuration. Needs to be added to config services. +* Query configurator rework #### Added * CorsPreflightHandlerInterface - resolve multiple service registered handler error diff --git a/src/Output/Configurator/ConfiguratorInterface.php b/src/Output/Configurator/ConfiguratorInterface.php index 01c817e..64e4e89 100644 --- a/src/Output/Configurator/ConfiguratorInterface.php +++ b/src/Output/Configurator/ConfiguratorInterface.php @@ -4,11 +4,9 @@ namespace Tomaj\NetteApi\Output\Configurator; -use Nette\Application\Request; - interface ConfiguratorInterface { - public function validateSchema(?Request $request = null): bool; + public function validateSchema(): bool; - public function showErrorDetail(?Request $request = null): bool; + public function showErrorDetail(): bool; } diff --git a/src/Output/Configurator/DebuggerConfigurator.php b/src/Output/Configurator/DebuggerConfigurator.php index 6315210..3a850c4 100644 --- a/src/Output/Configurator/DebuggerConfigurator.php +++ b/src/Output/Configurator/DebuggerConfigurator.php @@ -4,17 +4,16 @@ namespace Tomaj\NetteApi\Output\Configurator; -use Nette\Application\Request; use Tracy\Debugger; class DebuggerConfigurator implements ConfiguratorInterface { - public function validateSchema(?Request $request = null): bool + public function validateSchema(): bool { return !Debugger::$productionMode; } - public function showErrorDetail(?Request $request = null): bool + public function showErrorDetail(): bool { return !Debugger::$productionMode; } diff --git a/src/Output/Configurator/EnvConfigurator.php b/src/Output/Configurator/EnvConfigurator.php index 067b274..51d07f4 100644 --- a/src/Output/Configurator/EnvConfigurator.php +++ b/src/Output/Configurator/EnvConfigurator.php @@ -4,8 +4,6 @@ namespace Tomaj\NetteApi\Output\Configurator; -use Nette\Application\Request; - class EnvConfigurator implements ConfiguratorInterface { private $envVariable = 'APP_ENV'; @@ -21,7 +19,7 @@ public function __construct(string $envVariable = 'APP_ENV', string $productionV $this->productionValue = $productionValue; } - public function validateSchema(?Request $request = null): bool + public function validateSchema(): bool { $appEnv = getenv($this->envVariable); if ($appEnv === $this->productionValue) { @@ -30,7 +28,7 @@ public function validateSchema(?Request $request = null): bool return true; } - public function showErrorDetail(?Request $request = null): bool + public function showErrorDetail(): bool { $appEnv = getenv($this->envVariable); if ($appEnv === $this->productionValue) { diff --git a/src/Output/Configurator/QueryConfigurator.php b/src/Output/Configurator/QueryConfigurator.php index 97efcf4..0a4fba1 100644 --- a/src/Output/Configurator/QueryConfigurator.php +++ b/src/Output/Configurator/QueryConfigurator.php @@ -4,38 +4,37 @@ namespace Tomaj\NetteApi\Output\Configurator; -use Nette\Application\Request; +use Nette\Http\Request; class QueryConfigurator implements ConfiguratorInterface { - private $noSchemaValidateParam = 'no_schema_validate'; + private $schemaValidateParam = 'schema_validate'; private $errorDetailParam = 'error_detail'; + public $request = null; /** - * @param string $noSchemaValidateParam Name of get parameter to disable schema validation + * @param string $schemaValidateParam Name of get parameter to enable schema validation * @param string $errorDetailParam Name of get parameter to show additional info in error response */ - public function __construct(string $noSchemaValidateParam = 'no_schema_validate', string $errorDetailParam = 'error_detail') - { - $this->noSchemaValidateParam = $noSchemaValidateParam; + public function __construct( + Request $request, + string $schemaValidateParam = 'schema_validate', + string $errorDetailParam = 'error_detail' + ) { + $this->request = $request; + $this->schemaValidateParam = $schemaValidateParam; $this->errorDetailParam = $errorDetailParam; } - public function validateSchema(?Request $request = null): bool + public function validateSchema(): bool { - if ($request === null) { - return false; - } - $getParams = $request->getParameters(); - return !isset($getParams[$this->noSchemaValidateParam]); + $getParam = $this->request->getQuery($this->schemaValidateParam); + return $getParam !== null && $getParam !== '0' && $getParam !== 'false'; } - public function showErrorDetail(?Request $request = null): bool + public function showErrorDetail(): bool { - if ($request === null) { - return false; - } - $getParams = $request->getParameters(); - return isset($getParams[$this->errorDetailParam]); + $getParam = $this->request->getQuery($this->errorDetailParam); + return $getParam !== null && $getParam !== '0' && $getParam !== 'false'; } } diff --git a/src/Presenters/ApiPresenter.php b/src/Presenters/ApiPresenter.php index 797611e..5998b8c 100644 --- a/src/Presenters/ApiPresenter.php +++ b/src/Presenters/ApiPresenter.php @@ -87,7 +87,7 @@ public function run(Request $request): IResponse $paramsProcessor = new ParamsProcessor($handler->params()); if ($paramsProcessor->isError()) { $this->response->setCode(Response::S400_BAD_REQUEST); - if ($this->outputConfigurator->showErrorDetail($request)) { + if ($this->outputConfigurator->showErrorDetail()) { $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input', 'detail' => $paramsProcessor->getErrors()]); } else { $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input']); @@ -99,7 +99,7 @@ public function run(Request $request): IResponse $response = $handler->handle($params); $code = $response->getCode(); - if ($this->outputConfigurator->validateSchema($request)) { + if ($this->outputConfigurator->validateSchema()) { $outputValid = count($handler->outputs()) === 0; // back compatibility for handlers with no outputs defined $outputValidatorErrors = []; foreach ($handler->outputs() as $output) { @@ -120,7 +120,7 @@ public function run(Request $request): IResponse } } } catch (Throwable $exception) { - if ($this->outputConfigurator->showErrorDetail($request)) { + if ($this->outputConfigurator->showErrorDetail()) { $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $exception->getMessage()]); } else { $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']); From 01ed1d418f450bba3ec41638165805d4cc668324 Mon Sep 17 00:00:00 2001 From: KavajNaruj Date: Wed, 28 Aug 2024 14:35:14 +0200 Subject: [PATCH 10/12] Call $handler->outputs() method only once (#152) --- src/Presenters/ApiPresenter.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Presenters/ApiPresenter.php b/src/Presenters/ApiPresenter.php index 5998b8c..254bec9 100644 --- a/src/Presenters/ApiPresenter.php +++ b/src/Presenters/ApiPresenter.php @@ -100,9 +100,10 @@ public function run(Request $request): IResponse $code = $response->getCode(); if ($this->outputConfigurator->validateSchema()) { - $outputValid = count($handler->outputs()) === 0; // back compatibility for handlers with no outputs defined + $outputs = $handler->outputs(); + $outputValid = count($outputs) === 0; // back compatibility for handlers with no outputs defined $outputValidatorErrors = []; - foreach ($handler->outputs() as $output) { + foreach ($outputs as $output) { if (!$output instanceof OutputInterface) { $outputValidatorErrors[] = ["Output does not implement OutputInterface"]; continue; From d7c27bc20db53a6ca1f516e013789e3791def0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ber=C3=A1nek?= Date: Sat, 16 Nov 2024 23:50:04 +0100 Subject: [PATCH 11/12] Custom error handler (#153) --- CHANGELOG.md | 1 + README.md | 9 +++- src/Error/DefaultErrorHandler.php | 66 +++++++++++++++++++++++++++ src/Error/ErrorHandlerInterface.php | 39 ++++++++++++++++ src/Presenters/ApiPresenter.php | 54 +++++++++++----------- tests/Presenters/ApiPresenterTest.php | 5 ++ 6 files changed, 147 insertions(+), 27 deletions(-) create mode 100644 src/Error/DefaultErrorHandler.php create mode 100644 src/Error/ErrorHandlerInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b53b404..43f33e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip * [BC] DefaultHandler response code 404 instead 400 * [BC] Added Container to API Decider * [BC] Output Configurator, Allows different methods for output configuration. Needs to be added to config services. +* [BC] Error handler, Allows for custom error handling of handle method. Needs to be added to config services. * Query configurator rework #### Added diff --git a/README.md b/README.md index f164740..4ea800e 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,20 @@ application: Api: Tomaj\NetteApi\Presenters\*Presenter ``` -Then register your preffered output configurator in *config.neon* services: +Register your preferred output configurator in *config.neon* services: ```neon services: apiOutputConfigurator: Tomaj\NetteApi\Output\Configurator\DebuggerConfigurator ``` +Register your preferred error handler in *config.neon* services: + +```neon +services: + apiErrorHandler: Tomaj\NetteApi\Error\DefaultErrorHandler +``` + And add route to you RouterFactory: ```php diff --git a/src/Error/DefaultErrorHandler.php b/src/Error/DefaultErrorHandler.php new file mode 100644 index 0000000..9dcb512 --- /dev/null +++ b/src/Error/DefaultErrorHandler.php @@ -0,0 +1,66 @@ +outputConfigurator = $outputConfigurator; + } + + public function handle(Throwable $exception, array $params): JsonApiResponse + { + Debugger::log($exception, Debugger::EXCEPTION); + if ($this->outputConfigurator->showErrorDetail()) { + $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $exception->getMessage()]); + } else { + $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']); + } + return $response; + } + + public function handleInputParams(array $errors): JsonApiResponse + { + if ($this->outputConfigurator->showErrorDetail()) { + $response = new JsonApiResponse(Response::S400_BAD_REQUEST, ['status' => 'error', 'message' => 'wrong input', 'detail' => $errors]); + } else { + $response = new JsonApiResponse(Response::S400_BAD_REQUEST, ['status' => 'error', 'message' => 'wrong input']); + } + return $response; + } + + public function handleSchema(array $errors, array $params): JsonApiResponse + { + Debugger::log($errors, Debugger::ERROR); + + if ($this->outputConfigurator->showErrorDetail()) { + $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $errors]); + } else { + $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']); + } + return $response; + } + + public function handleAuthorization(ApiAuthorizationInterface $auth, array $params): JsonApiResponse + { + return new JsonApiResponse(Response::S401_UNAUTHORIZED, ['status' => 'error', 'message' => $auth->getErrorMessage()]); + } + + public function handleAuthorizationException(Throwable $exception, array $params): JsonApiResponse + { + return new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => $exception->getMessage()]); + } +} diff --git a/src/Error/ErrorHandlerInterface.php b/src/Error/ErrorHandlerInterface.php new file mode 100644 index 0000000..91974e4 --- /dev/null +++ b/src/Error/ErrorHandlerInterface.php @@ -0,0 +1,39 @@ + $params + */ + public function handle(Throwable $exception, array $params): JsonApiResponse; + + /** + * @param array $errors + * @param array $params + */ + public function handleInputParams(array $errors): JsonApiResponse; + + /** + * @param array $errors + * @param array $params + */ + public function handleSchema(array $errors, array $params): JsonApiResponse; + + /** + * @param array $params + */ + public function handleAuthorization(ApiAuthorizationInterface $auth, array $params): JsonApiResponse; + + /** + * @param array $params + */ + public function handleAuthorizationException(Throwable $exception, array $params): JsonApiResponse; +} diff --git a/src/Presenters/ApiPresenter.php b/src/Presenters/ApiPresenter.php index 254bec9..ea32b33 100644 --- a/src/Presenters/ApiPresenter.php +++ b/src/Presenters/ApiPresenter.php @@ -14,14 +14,13 @@ use Tomaj\NetteApi\Api; use Tomaj\NetteApi\ApiDecider; use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface; +use Tomaj\NetteApi\Error\ErrorHandlerInterface; use Tomaj\NetteApi\Logger\ApiLoggerInterface; use Tomaj\NetteApi\Misc\IpDetectorInterface; +use Tomaj\NetteApi\Output\Configurator\ConfiguratorInterface; use Tomaj\NetteApi\Output\OutputInterface; use Tomaj\NetteApi\Params\ParamsProcessor; use Tomaj\NetteApi\RateLimit\RateLimitInterface; -use Tomaj\NetteApi\Response\JsonApiResponse; -use Tracy\Debugger; -use Tomaj\NetteApi\Output\Configurator\ConfiguratorInterface; final class ApiPresenter implements IPresenter { @@ -37,6 +36,9 @@ final class ApiPresenter implements IPresenter /** @var ConfiguratorInterface @inject */ public $outputConfigurator; + /** @var ErrorHandlerInterface @inject */ + public $errorHandler; + /** * CORS header settings * @@ -71,14 +73,10 @@ public function run(Request $request): IResponse $api = $this->getApi($request); $handler = $api->getHandler(); + $authorization = $api->getAuthorization(); $rateLimit = $api->getRateLimit(); - $authResponse = $this->checkAuth($authorization); - if ($authResponse !== null) { - return $authResponse; - } - $rateLimitResponse = $this->checkRateLimit($rateLimit); if ($rateLimitResponse !== null) { return $rateLimitResponse; @@ -86,15 +84,17 @@ public function run(Request $request): IResponse $paramsProcessor = new ParamsProcessor($handler->params()); if ($paramsProcessor->isError()) { - $this->response->setCode(Response::S400_BAD_REQUEST); - if ($this->outputConfigurator->showErrorDetail()) { - $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input', 'detail' => $paramsProcessor->getErrors()]); - } else { - $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input']); - } + $response = $this->errorHandler->handleInputParams($paramsProcessor->getErrors()); + $this->response->setCode($response->getCode()); return $response; } $params = $paramsProcessor->getValues(); + + $authResponse = $this->checkAuth($authorization, $params); + if ($authResponse !== null) { + return $authResponse; + } + try { $response = $handler->handle($params); $code = $response->getCode(); @@ -116,18 +116,13 @@ public function run(Request $request): IResponse $outputValidatorErrors[] = $validationResult->getErrors(); } if (!$outputValid) { - Debugger::log($outputValidatorErrors, Debugger::ERROR); - $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'details' => $outputValidatorErrors]); + $response = $this->errorHandler->handleSchema($outputValidatorErrors, $params); + $code = $response->getCode(); } } } catch (Throwable $exception) { - if ($this->outputConfigurator->showErrorDetail()) { - $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $exception->getMessage()]); - } else { - $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']); - } + $response = $this->errorHandler->handle($exception, $params); $code = $response->getCode(); - Debugger::log($exception, Debugger::EXCEPTION); } $end = microtime(true); @@ -153,11 +148,18 @@ private function getApi(Request $request): Api ); } - private function checkAuth(ApiAuthorizationInterface $authorization): ?IResponse + private function checkAuth(ApiAuthorizationInterface $authorization, array $params): ?IResponse { - if (!$authorization->authorized()) { - $this->response->setCode(Response::S403_FORBIDDEN); - return new JsonResponse(['status' => 'error', 'message' => $authorization->getErrorMessage()]); + try { + if (!$authorization->authorized()) { + $response = $this->errorHandler->handleAuthorization($authorization, $params); + $this->response->setCode($response->getCode()); + return $response; + } + } catch (Throwable $exception) { + $response = $this->errorHandler->handleAuthorizationException($exception, $params); + $this->response->setCode($response->getCode()); + return $response; } return null; } diff --git a/tests/Presenters/ApiPresenterTest.php b/tests/Presenters/ApiPresenterTest.php index 804ace4..2d194d5 100644 --- a/tests/Presenters/ApiPresenterTest.php +++ b/tests/Presenters/ApiPresenterTest.php @@ -12,6 +12,7 @@ use Tomaj\NetteApi\Authorization\BearerTokenAuthorization; use Tomaj\NetteApi\Authorization\NoAuthorization; use Tomaj\NetteApi\EndpointIdentifier; +use Tomaj\NetteApi\Error\DefaultErrorHandler; use Tomaj\NetteApi\Handlers\AlwaysOkHandler; use Tomaj\NetteApi\Handlers\EchoHandler; use Tomaj\NetteApi\Misc\IpDetector; @@ -41,6 +42,7 @@ public function testSimpleResponse() $presenter->response = new HttpResponse(); $presenter->context = new Container(); $presenter->outputConfigurator = new DebuggerConfigurator(); + $presenter->errorHandler = new DefaultErrorHandler($presenter->outputConfigurator); $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); @@ -65,6 +67,7 @@ public function testWithAuthorization() $presenter->response = new HttpResponse(); $presenter->context = new Container(); $presenter->outputConfigurator = new DebuggerConfigurator(); + $presenter->errorHandler = new DefaultErrorHandler($presenter->outputConfigurator); $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); @@ -87,6 +90,7 @@ public function testWithParams() $presenter->response = new HttpResponse(); $presenter->context = new Container(); $presenter->outputConfigurator = new DebuggerConfigurator(); + $presenter->errorHandler = new DefaultErrorHandler($presenter->outputConfigurator); Debugger::$productionMode = Debugger::PRODUCTION; @@ -117,6 +121,7 @@ public function testWithOutputs() $presenter->response = new HttpResponse(); $presenter->context = new Container(); $presenter->outputConfigurator = new DebuggerConfigurator(); + $presenter->errorHandler = new DefaultErrorHandler($presenter->outputConfigurator); $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); From e9a1edfe0e398af2d16aba084d8b592d886b81d2 Mon Sep 17 00:00:00 2001 From: Michal Lulco Date: Sun, 12 Jan 2025 14:53:42 +0100 Subject: [PATCH 12/12] Fixed version in api listing control --- src/Component/ApiListingControl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Component/ApiListingControl.php b/src/Component/ApiListingControl.php index 6628f95..4965a98 100644 --- a/src/Component/ApiListingControl.php +++ b/src/Component/ApiListingControl.php @@ -37,7 +37,7 @@ public function render(): void $template->render(); } - public function handleSelect(string $method, int $version, string $package, ?string $apiAction = null): void + public function handleSelect(string $method, $version, string $package, ?string $apiAction = null): void { $this->onClick($method, $version, $package, $apiAction); }