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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ class UsersTest extends TestCase
}

```
### PSR-15 middleware
Any PSR-15 stack (Slim, Mezzio and others) can verify its responses with the middleware. A response that does not
match throws `OpenApiSchemaMismatchException`. An optional callback is called with the exception and the response
first, to report the mismatch in a way of your own.

```php
use Radebatz\OpenApi\Verifier\Adapters\Psr15\OpenApiVerifierMiddleware;
use Radebatz\OpenApi\Verifier\OpenApiVerifier;

$app->add(new OpenApiVerifierMiddleware(OpenApiVerifier::fromSpecification('openapi.yaml')));
```

### Laravel adapter
The adapter will try to resolve the specification dynamically in this order:
* filename passed into `registerOpenApiVerifier()`
Expand Down Expand Up @@ -171,7 +183,7 @@ The adapter will try to resolve the specification dynamically in this order:

Simplest way is to register the verifier in the `Tests\Functional\BaseTestCase`.

**Attention**: In order to be able to resolve routes with placeholders (i.e. something like `/user/{id}`) it is required to register the verifier **before** the routing middleware.
The adapter adds the PSR-15 middleware below, and a mismatch fails the test with the error summary and the response body.

```php
<?php
Expand All @@ -192,7 +204,7 @@ class BaseTestCase extends TestCase

$app = new App();

// register OpenApi verifier
// register OpenApi verifier, before or after the routing middleware
$this->registerOpenApiVerifier($app, __DIR__ . '/../specifications/users.yaml');
$app->addRoutingMiddleware();

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"nyholm/psr7-server": "^1.0",
"psr/http-client": "^1.0",
"psr/http-message": "^2.0",
"psr/http-server-middleware": "^1.0",
"symfony/psr-http-message-bridge": "^6.0 || ^7.0"
},
"require-dev": {
Expand Down
42 changes: 0 additions & 42 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,12 @@ parameters:
count: 1
path: src/Adapters/AbstractOpenApiResponseVerifier.php

-
message: '#^Call to method failSchemaMismatch\(\) on an unknown class Radebatz\\OpenApi\\Verifier\\VerifiesOpenApi\.$#'
identifier: class.notFound
count: 1
path: src/Adapters/Laravel/OpenApiVerifierMiddleware.php

-
message: '#^Call to method verifyOpenApi\(\) on an unknown class Radebatz\\OpenApi\\Verifier\\VerifiesOpenApi\.$#'
identifier: class.notFound
count: 1
path: src/Adapters/Laravel/OpenApiVerifierMiddleware.php

-
message: '#^PHPDoc tag @var for variable \$verifier has invalid type Radebatz\\OpenApi\\Verifier\\VerifiesOpenApi\.$#'
identifier: varTag.trait
count: 1
path: src/Adapters/Laravel/OpenApiVerifierMiddleware.php

-
message: '#^Call to method failSchemaMismatch\(\) on an unknown class Radebatz\\OpenApi\\Verifier\\VerifiesOpenApi\.$#'
identifier: class.notFound
count: 1
path: src/Adapters/Slim/OpenApiVerifierMiddleware.php

-
message: '#^Call to method verifyOpenApi\(\) on an unknown class Radebatz\\OpenApi\\Verifier\\VerifiesOpenApi\.$#'
identifier: class.notFound
count: 1
path: src/Adapters/Slim/OpenApiVerifierMiddleware.php

-
message: '#^PHPDoc tag @var for variable \$verifier has invalid type Radebatz\\OpenApi\\Verifier\\VerifiesOpenApi\.$#'
identifier: varTag.trait
count: 1
path: src/Adapters/Slim/OpenApiVerifierMiddleware.php

-
message: '#^Negated boolean expression is always false\.$#'
identifier: booleanNot.alwaysFalse
count: 1
path: tests/Adapters/LaravelAdapterTest.php

-
message: '#^Access to an undefined property Radebatz\\OpenApi\\Verifier\\Tests\\Adapters\\SlimAdapterTest\:\:\$app\.$#'
identifier: property.notFound
count: 1
path: tests/Adapters/SlimAdapterTest.php

-
message: '#^Right side of \|\| is always false\.$#'
identifier: booleanOr.rightAlwaysFalse
Expand Down
6 changes: 2 additions & 4 deletions src/Adapters/AbstractOpenApiResponseVerifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
namespace Radebatz\OpenApi\Verifier\Adapters;

use OpenApi\Generator;
use Radebatz\OpenApi\Verifier\OpenApiSpecificationLoader;

trait AbstractOpenApiResponseVerifier
{
protected ?string $openapiSpecification;
protected object|string|null $openapiSpecification = null;

protected function prepareOpenApiSpecificationLoader(string $srcDir, ?string $specification = null)
{
Expand Down Expand Up @@ -38,9 +37,8 @@ protected function prepareOpenApiSpecificationLoader(string $srcDir, ?string $sp
if (!$specificationLoader) {
$appRoot = $appRoot ?: $this->getAppRoot();

// the generated document is an object, which the ?string property cannot hold
$openApi = (new Generator())->generate([$appRoot . '/' . $srcDir]);
$this->openapiSpecificationLoader = new OpenApiSpecificationLoader(json_decode($openApi->toJson()));
$this->openapiSpecification = json_decode($openApi->toJson());
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/Adapters/Laravel/OpenApiResponseVerifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use Illuminate\Contracts\Http\Kernel;
use Illuminate\Foundation\Application;
use Psr\Http\Message\ResponseInterface;
use Radebatz\OpenApi\Verifier\Adapters\AbstractOpenApiResponseVerifier;
use Radebatz\OpenApi\Verifier\OpenApiSchemaMismatchException;
use Radebatz\OpenApi\Verifier\VerifiesOpenApi;

/**
Expand All @@ -14,14 +16,18 @@ trait OpenApiResponseVerifier
{
use AbstractOpenApiResponseVerifier, VerifiesOpenApi;

public function registerOpenApiVerifier(?Application $app = null, ?string $specification = null)
public function registerOpenApiVerifier(?Application $app = null, ?string $specification = null): void
{
$app = $app ?: $this->app;

$this->prepareOpenApiSpecificationLoader('app', $specification);

if ($this->getOpenApiSpecificationLoader()) {
$app->instance(OpenApiVerifierMiddleware::OPENAPI_VERFIER_CONTAINER_KEY, $this);
// the kernel resolves the middleware by class name, for `handle()` and `terminate()` alike
$app->instance(OpenApiVerifierMiddleware::class, new OpenApiVerifierMiddleware(
$this->getOpenApiVerifier(),
fn (OpenApiSchemaMismatchException $oasme, ResponseInterface $response) => $this->failSchemaMismatch($oasme, $response)
));
$app[Kernel::class]->pushMiddleware(OpenApiVerifierMiddleware::class);
}
}
Expand Down
48 changes: 28 additions & 20 deletions src/Adapters/Laravel/OpenApiVerifierMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,51 @@

namespace Radebatz\OpenApi\Verifier\Adapters\Laravel;

use Radebatz\OpenApi\Verifier\Adapters\PSR17Middleware;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ResponseInterface;
use Radebatz\OpenApi\Verifier\OpenApiSchemaMismatchException;
use Radebatz\OpenApi\Verifier\VerifiesOpenApi;
use Radebatz\OpenApi\Verifier\OpenApiVerifier;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;

/**
* Terminating middleware to verify OpenApi responses,.
* Terminating middleware verifying every response against the specification.
*
* Laravel is not PSR-15, so the request and response are converted to PSR-7 first. The route's
* uri is passed as the path template, since Laravel has already matched it.
*/
class OpenApiVerifierMiddleware extends PSR17Middleware
final class OpenApiVerifierMiddleware
{
public const OPENAPI_VERFIER_CONTAINER_KEY = 'openapi-verifier';
private readonly PsrHttpFactory $psrHttpFactory;

/**
* @param (\Closure(OpenApiSchemaMismatchException, ResponseInterface): void)|null $onMismatch
*/
public function __construct(
private readonly OpenApiVerifier $verifier,
private readonly ?\Closure $onMismatch = null,
) {
$psr17Factory = new Psr17Factory();
$this->psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
}

public function handle($request, \Closure $next)
{
return $next($request);
}

public function terminate($request, $response)
public function terminate($request, $response): void
{
/** @var VerifiesOpenApi $verifier */
$verifier = app(OpenApiVerifierMiddleware::OPENAPI_VERFIER_CONTAINER_KEY);
$psrRequest = $this->psrHttpFactory->createRequest($request);
$psrResponse = $this->psrHttpFactory->createResponse($response);

try {
$psrRequest = $this->psrHttpFactory->createRequest($request);
$psrResponse = $this->psrHttpFactory->createResponse($response);

$routePath = null;
if ($route = $request->route()) {
$routePath = $route->uri;
}

$verifier->verifyOpenApi($psrRequest, $psrResponse, $routePath);
$this->verifier->verify($psrRequest, $psrResponse, $request->route()?->uri);
} catch (OpenApiSchemaMismatchException $oasme) {
$verifier->failSchemaMismatch($oasme, $psrResponse);
if ($this->onMismatch instanceof \Closure) {
($this->onMismatch)($oasme, $psrResponse);
}

throw $oasme;
}

return $response;
}
}
17 changes: 0 additions & 17 deletions src/Adapters/PSR17Middleware.php

This file was deleted.

45 changes: 45 additions & 0 deletions src/Adapters/Psr15/OpenApiVerifierMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php declare(strict_types=1);

namespace Radebatz\OpenApi\Verifier\Adapters\Psr15;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Radebatz\OpenApi\Verifier\OpenApiSchemaMismatchException;
use Radebatz\OpenApi\Verifier\OpenApiVerifier;

/**
* PSR-15 middleware verifying every response against the specification.
*
* Throws `OpenApiSchemaMismatchException` on a mismatch. `$onMismatch` is called first, which lets a
* test case turn the mismatch into a failure of its own.
*/
final class OpenApiVerifierMiddleware implements MiddlewareInterface
{
/**
* @param (\Closure(OpenApiSchemaMismatchException, ResponseInterface): void)|null $onMismatch
*/
public function __construct(
private readonly OpenApiVerifier $verifier,
private readonly ?\Closure $onMismatch = null,
) {
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);

try {
$this->verifier->verify($request, $response);
} catch (OpenApiSchemaMismatchException $oasme) {
if ($this->onMismatch instanceof \Closure) {
($this->onMismatch)($oasme, $response);
}

throw $oasme;
}

return $response;
}
}
25 changes: 8 additions & 17 deletions src/Adapters/Slim/OpenApiResponseVerifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,26 @@

namespace Radebatz\OpenApi\Verifier\Adapters\Slim;

use Psr\Http\Message\ResponseInterface;
use Radebatz\OpenApi\Verifier\Adapters\AbstractOpenApiResponseVerifier;
use Radebatz\OpenApi\Verifier\Adapters\Psr15\OpenApiVerifierMiddleware;
use Radebatz\OpenApi\Verifier\OpenApiSchemaMismatchException;
use Radebatz\OpenApi\Verifier\VerifiesOpenApi;
use Slim\App;

trait OpenApiResponseVerifier
{
use AbstractOpenApiResponseVerifier, VerifiesOpenApi;

public function registerOpenApiVerifier(?App $app, ?string $specification = null)
public function registerOpenApiVerifier(App $app, ?string $specification = null): void
{
/** @var App $app */
$app = $app ?: $this->app;

$this->prepareOpenApiSpecificationLoader('src', $specification);

if ($this->getOpenApiSpecificationLoader()) {
$container = $app->getContainer();

// deal with different container implementations
if ($container instanceof \ArrayAccess) {
$container[OpenApiVerifierMiddleware::OPENAPI_VERFIER_CONTAINER_KEY] = $this;
} elseif (method_exists($container, 'set')) {
$container->set(OpenApiVerifierMiddleware::OPENAPI_VERFIER_CONTAINER_KEY, $this);
} elseif (method_exists($container, 'add')) {
$container->add(OpenApiVerifierMiddleware::OPENAPI_VERFIER_CONTAINER_KEY, $this);
} else {
throw new \RuntimeException('Unusable container');
}
$app->add(new OpenApiVerifierMiddleware($container));
$app->add(new OpenApiVerifierMiddleware(
$this->getOpenApiVerifier(),
fn (OpenApiSchemaMismatchException $oasme, ResponseInterface $response) => $this->failSchemaMismatch($oasme, $response)
));
}
}
}
45 changes: 0 additions & 45 deletions src/Adapters/Slim/OpenApiVerifierMiddleware.php

This file was deleted.

Loading
Loading