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
31 changes: 0 additions & 31 deletions phpstan-baseline.neon

This file was deleted.

5 changes: 1 addition & 4 deletions phpstan.neon → phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
includes:
- phpstan-baseline.neon

parameters:
phpVersion: 80200
level: 5
level: 7
paths:
- src
- tests
Expand Down
50 changes: 25 additions & 25 deletions src/Adapters/AbstractOpenApiResponseVerifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,44 @@ trait AbstractOpenApiResponseVerifier
{
protected object|string|null $openapiSpecification = null;

protected function prepareOpenApiSpecificationLoader(string $srcDir, ?string $specification = null)
/**
* Find the specification: the given one, then `tests/openapi.json` or `tests/openapi.yaml`,
* then one generated from `$srcDir`.
*/
protected function prepareOpenApiSpecificationLoader(string $srcDir, ?string $specification = null): void
{
$appRoot = null;

if ($specification) {
$this->openapiSpecification = $specification;
}

// try loader
$specificationLoader = $this->getOpenApiSpecificationLoader();
if ($this->getOpenApiSpecificationLoader()) {
return;
}

$appRoot = $this->getAppRoot();

if (!$specificationLoader) {
$appRoot = $appRoot ?: $this->getAppRoot();
foreach (['openapi.json', 'openapi.yaml'] as $specfile) {
if (file_exists($filename = $appRoot . '/tests/' . $specfile)) {
$this->openapiSpecification = $filename;

// try some default filenames
foreach (['openapi.json', 'openapi.yaml'] as $specfile) {
if (file_exists($specification = $appRoot . '/tests/' . $specfile)) {
$this->openapiSpecification = $specification;
break;
}
return;
}

// try loader again
$specificationLoader = $this->getOpenApiSpecificationLoader();
}

if (!$specificationLoader) {
$appRoot = $appRoot ?: $this->getAppRoot();

$openApi = (new Generator())->generate([$appRoot . '/' . $srcDir]);
$this->openapiSpecification = json_decode($openApi->toJson());
}
$openApi = (new Generator())->generate([$appRoot . '/' . $srcDir]);
$this->openapiSpecification = json_decode($openApi->toJson());
}

protected function getAppRoot()
/**
* The project root: three levels above Composer's `ClassLoader`, in `vendor/composer/`.
*/
protected function getAppRoot(): string
{
$rc = new \ReflectionClass('\Composer\Autoload\ClassLoader');
$classLoader = (new \ReflectionClass(\Composer\Autoload\ClassLoader::class))->getFileName();
if (false === $classLoader) {
throw new \RuntimeException('Cannot locate the project root: Composer\'s ClassLoader has no file');
}

return dirname(dirname(dirname($rc->getFileName())));
return dirname($classLoader, 3);
}
}
12 changes: 9 additions & 3 deletions src/Adapters/Laravel/OpenApiVerifierMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

namespace Radebatz\OpenApi\Verifier\Adapters\Laravel;

use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ResponseInterface;
use Radebatz\OpenApi\Verifier\OpenApiSchemaMismatchException;
use Radebatz\OpenApi\Verifier\OpenApiVerifier;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpFoundation\Response;

/**
* Terminating middleware verifying every response against the specification.
Expand All @@ -29,18 +32,21 @@ public function __construct(
$this->psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
}

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

public function terminate($request, $response): void
public function terminate(Request $request, Response $response): void
{
$psrRequest = $this->psrHttpFactory->createRequest($request);
$psrResponse = $this->psrHttpFactory->createResponse($response);

try {
$this->verifier->verify($psrRequest, $psrResponse, $request->route()?->uri);
// what `route()` calls; its docblock claims a route is always there, but a request that
// matched none, a 404 for example, has none
$route = ($request->getRouteResolver())();
$this->verifier->verify($psrRequest, $psrResponse, $route instanceof Route ? $route->uri() : null);
} catch (OpenApiSchemaMismatchException $oasme) {
if ($this->onMismatch instanceof \Closure) {
($this->onMismatch)($oasme, $psrResponse);
Expand Down
5 changes: 5 additions & 0 deletions src/Adapters/Slim/OpenApiResponseVerifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ trait OpenApiResponseVerifier
{
use AbstractOpenApiResponseVerifier, VerifiesOpenApi;

/**
* @template TContainer of \Psr\Container\ContainerInterface|null
*
* @param App<TContainer> $app
*/
public function registerOpenApiVerifier(App $app, ?string $specification = null): void
{
$this->prepareOpenApiSpecificationLoader('src', $specification);
Expand Down
24 changes: 13 additions & 11 deletions src/OpenApiSpecificationLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,21 @@

class OpenApiSpecificationLoader
{
protected $filename;
protected $specification;
protected \stdClass $specification;

/**
* @param $specification object|string The specification object or filename
* @param mixed $specification the specification object, or the filename of a JSON or YAML document
*/
public function __construct($specification)
public function __construct(mixed $specification)
{
$this->specification = $this->resolveSpecification($specification);
}

protected function resolveSpecification($specification)
protected function resolveSpecification(mixed $specification): \stdClass
{
$resolved = null;
if (is_string($specification)) {
if (false !== strpos($specification, '.yaml') || false !== strpos($specification, '.yml')) {
if (preg_match('/\.ya?ml$/i', $specification)) {
try {
$resolved = (object) Yaml::parseFile($specification, Yaml::PARSE_OBJECT_FOR_MAP);
} catch (ParseException $parseException) {
Expand All @@ -32,15 +31,18 @@ protected function resolveSpecification($specification)
$parseException
);
}
} elseif (file_exists($specification)) {
$resolved = json_decode(file_get_contents($specification));
} elseif (file_exists($specification) && false !== ($json = file_get_contents($specification))) {
$resolved = json_decode($json);
}

if (!$resolved) {
if (!$resolved instanceof \stdClass) {
throw new \InvalidArgumentException(sprintf('Could not load specification: %s', $specification));
}
} elseif (is_object($specification)) {
} elseif ($specification instanceof \stdClass) {
$resolved = $specification;
} elseif (is_object($specification)) {
// any other object, as plain data
$resolved = json_decode((string) json_encode($specification));
} else {
throw new \InvalidArgumentException('Invalid specification');
}
Expand Down Expand Up @@ -78,7 +80,7 @@ public function getResponseSchemaFor(string $method, string $path, int $statusCo
}

// copy, so the components do not end up inside the specification itself
$schema = is_object($schema) ? clone $schema : (object) $schema;
$schema = $schema instanceof \stdClass ? clone $schema : (object) (array) $schema;
$schema->components = $this->property($this->specification, 'components') ?? new \stdClass();

return $schema;
Expand Down
3 changes: 2 additions & 1 deletion src/Schema/OpenApi30Translator.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ private function translateNode(mixed $node): mixed
return array_map($this->translateNode(...), $node);
}

if (!is_object($node)) {
// the copy comes from `json_decode()`, so every object in it is a `stdClass`
if (!$node instanceof \stdClass) {
return $node;
}

Expand Down
8 changes: 4 additions & 4 deletions tests/Adapters/AbstractOpenApiResponseVerifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ protected function tearDown(): void
}
}

protected function getAppRoot()
protected function getAppRoot(): string
{
return $this->appRoot;
}
Expand All @@ -42,7 +42,7 @@ protected function assertVerifiesUsers(): void
}

#[Test]
public function explicitSpecification()
public function explicitSpecification(): void
{
$this->appRoot = '/does/not/exist';

Expand All @@ -52,7 +52,7 @@ public function explicitSpecification()
}

#[Test]
public function discoveredSpecification()
public function discoveredSpecification(): void
{
$this->tmpDir = sys_get_temp_dir() . '/openapi-verifier-' . uniqid();
mkdir($this->tmpDir . '/tests', 0777, true);
Expand All @@ -65,7 +65,7 @@ public function discoveredSpecification()
}

#[Test]
public function generatedSpecification()
public function generatedSpecification(): void
{
// no tests/openapi.{json,yaml} under tests/, so this generates from tests/Fixtures
$this->appRoot = dirname(__DIR__);
Expand Down
3 changes: 3 additions & 0 deletions tests/Adapters/ClientAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ protected function verifier(): OpenApiVerifier
return OpenApiVerifier::fromSpecification(__DIR__ . '/../specifications/users.yaml');
}

/**
* @return iterable<array<mixed>>
*/
public static function responses(): iterable
{
// path, body, expected error (null: valid)
Expand Down
45 changes: 20 additions & 25 deletions tests/Adapters/LaravelAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,18 @@

use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Testing\TestCase;
use Illuminate\Support\Facades\Facade;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Attributes\Test;
use Radebatz\OpenApi\Verifier\Adapters\Laravel\OpenApiResponseVerifier;

class LaravelAdapterTest extends LaravelTestCase
class LaravelAdapterTest extends TestCase
{
use OpenApiResponseVerifier;

protected function setUp(): void
{
if (!class_exists('\\Illuminate\\Foundation\\Application')) {
$this->markTestSkipped('Laravel not installed.');
}

parent::setUp();

$this->registerOpenApiVerifier(null, __DIR__ . '/../specifications/users.yaml');
Expand All @@ -27,7 +24,7 @@ protected function setUp(): void
#[Test]
public function passVerificationUsers(): void
{
$this->createApplication()
$this->app
->get('router')->get('/users', function () {
return '{"data":[{"id":1,"name":"joe","email":"joe@cool.com"}]}';
});
Expand All @@ -40,7 +37,7 @@ public function passVerificationUsers(): void
#[Test]
public function failVerificationUsers(): void
{
$this->createApplication()
$this->app
->get('router')->get('/users', function () {
return '{"data":[{"id":1,"email":"joe@cool.com"}]}';
});
Expand All @@ -53,7 +50,7 @@ public function failVerificationUsers(): void
#[Test]
public function passVerificationUser(): void
{
$this->createApplication()
$this->app
->get('router')->get('/user/{id}', function () {
return '{"data":{"id":1,"name":"joe","email":"joe@cool.com"}}';
});
Expand All @@ -66,7 +63,7 @@ public function passVerificationUser(): void
#[Test]
public function failVerificationUser(): void
{
$this->createApplication()
$this->app
->get('router')->get('/user/{id}', function () {
return '{"data":{"id":1,"name":"joe"}}';
});
Expand All @@ -77,22 +74,20 @@ public function failVerificationUser(): void
$this->get('/user/1');
}

/** @inheritdoc */
public function createApplication()
/**
* Laravel's `setUp()` calls this and keeps the result in `$this->app`.
*/
public function createApplication(): Application
{
if (!$this->app) {
/** @var Application $app */
$app = require __DIR__ . '/../../vendor/laravel/laravel/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$app['config']->set([
'app.environment' => 'local',
'app.debug' => true,
]);
Facade::setFacadeApplication($app);

$this->app = $app;
}

return $this->app;
/** @var Application $app */
$app = require __DIR__ . '/../../vendor/laravel/laravel/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$app['config']->set([
'app.environment' => 'local',
'app.debug' => true,
]);
Facade::setFacadeApplication($app);

return $app;
}
}
13 changes: 0 additions & 13 deletions tests/Adapters/LaravelTestCase.php

This file was deleted.

Loading
Loading