From f6989ca0919c42dc9c3baa2f0cccb4ac3ae81acb Mon Sep 17 00:00:00 2001 From: Daniel Jakob Date: Thu, 16 Oct 2025 08:07:00 +0200 Subject: [PATCH] Optionally allow JsonSchema 6.x This allows the installation of `justinrainbow/json-schema` ^6 and upgrades the `OpenApiSchemaMismatchException` to work with the modified error-item structure (see json-schema upgrading guide). Also this performs some minor refactorings in the exception as well as it adds a set of tests for it. --- composer.json | 2 +- phpstan-baseline.neon | 6 - src/OpenApiSchemaMismatchException.php | 49 +++++-- tests/OpenApiSchemaMismatchExceptionTest.php | 143 +++++++++++++++++++ tests/specifications/users.json | 2 +- 5 files changed, 181 insertions(+), 21 deletions(-) create mode 100644 tests/OpenApiSchemaMismatchExceptionTest.php diff --git a/composer.json b/composer.json index 1f4bbfc..c392d22 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ "require": { "php": ">=8.1", "ext-json": "*", - "justinrainbow/json-schema": "^5.2.13", + "justinrainbow/json-schema": "^5.2.13 || ^6", "nyholm/psr7": "^1.1", "nyholm/psr7-server": "^1.0", "psr/http-message": "^2.0", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ecacce7..00523a0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -36,12 +36,6 @@ parameters: count: 1 path: src/Adapters/Slim/OpenApiVerifierMiddleware.php - - - message: '#^Call to sprintf contains 2 placeholders, 1 value given\.$#' - identifier: argument.sprintf - count: 1 - path: src/OpenApiSchemaMismatchException.php - - message: '#^Call to function property_exists\(\) with \$this\(Radebatz\\OpenApi\\Verifier\\Tests\\Adapters\\LaravelAdapterTest\) and ''openapiSpecification'' will always evaluate to true\.$#' identifier: function.alreadyNarrowedType diff --git a/src/OpenApiSchemaMismatchException.php b/src/OpenApiSchemaMismatchException.php index f137702..65b1c0f 100644 --- a/src/OpenApiSchemaMismatchException.php +++ b/src/OpenApiSchemaMismatchException.php @@ -2,15 +2,31 @@ namespace Radebatz\OpenApi\Verifier; +/** + * @phpstan-type JsonSchemaValidationError array{ + * property: string, + * pointer: string, + * message: string, + * constraint: string|array{name: string, params: array}, + * context: int + * } + */ class OpenApiSchemaMismatchException extends \Exception { - protected $errors = []; + /** @var list */ + protected array $errors = []; + /** + * @return list + */ public function getErrors(): array { return $this->errors; } + /** + * @param list $errors + */ public function setErrors(array $errors): OpenApiSchemaMismatchException { $this->errors = $errors; @@ -29,32 +45,39 @@ public function getErrorSummary(): ?string foreach ($this->errors as $error) { $wildcarded = preg_replace('/\[[0-9]+\]/', '[*]', $error['property']); - $errorType = $error['constraint'] . '|' . $error['message']; + // ensures compatibility with both, JsonSchema 5.x and 6.x + $constraintName = $error['constraint']['name'] + ?? $error['constraint']; + + $errorType = sprintf('%s|%s', $constraintName, $error['message']); if (!array_key_exists($errorType, $errorTypeMap)) { $errorTypeMap[$errorType] = [ 'properties' => [ - $wildcarded => [$error['property']], + $wildcarded => 1, ], - 'constraint' => $error['constraint'], + 'constraint' => $constraintName, 'message' => $error['message'], ]; } else { - if (!in_array($wildcarded, $errorTypeMap[$errorType]['properties'])) { - $errorTypeMap[$errorType]['properties'][$wildcarded] = [$error['property']]; + // track usage of the same wildcard-property + if (!array_key_exists($wildcarded, $errorTypeMap[$errorType]['properties'])) { + $errorTypeMap[$errorType]['properties'][$wildcarded] = 1; } else { - $errorTypeMap[$errorType]['properties'][] = $wildcarded; + $errorTypeMap[$errorType]['properties'][$wildcarded]++; } } } $summary = []; - foreach ($errorTypeMap as $es) { - $summary[] = sprintf('%s - %s', $es['constraint'], $es['message']); - foreach ($es['properties'] as $ps => $pl) { - if (1 == count($pl)) { - $summary[] = sprintf(' - %s', $ps); + foreach ($errorTypeMap as $errorType) { + $summary[] = sprintf('%s - %s', $errorType['constraint'], $errorType['message']); + + foreach ($errorType['properties'] as $propertyName => $propertyAmount) { + // if the property errored multiple times, add the amount to the error-message + if (1 === $propertyAmount) { + $summary[] = sprintf(' - %s', $propertyName); } else { - $summary[] = sprintf(' - %s (%s more)', count($pl) - 1); + $summary[] = sprintf(' - %s (%s more)', $propertyName, $propertyAmount - 1); } } } diff --git a/tests/OpenApiSchemaMismatchExceptionTest.php b/tests/OpenApiSchemaMismatchExceptionTest.php new file mode 100644 index 0000000..1cebb28 --- /dev/null +++ b/tests/OpenApiSchemaMismatchExceptionTest.php @@ -0,0 +1,143 @@ +subject = new OpenApiSchemaMismatchException(); + } + + #[Test] + public function getErrorsReturnsSetData(): void + { + $value = [[ + 'property' => 'some-property', + 'pointer' => 'some-pointer', + 'message' => 'some-message', + 'constraint' => 'some-constraint', + 'context' => PHP_INT_MAX, + ]]; + + $this->subject->setErrors($value); + + static::assertSame( + $value, + $this->subject->getErrors(), + ); + } + + #[Test] + #[DataProvider('errorDataProvider')] + public function getErrorSummaryReturnsExpectedResult( + array $errors, + null|string $expected_result, + ): void { + $this->subject->setErrors($errors); + + static::assertSame( + $expected_result, + $this->subject->getErrorSummary(), + ); + } + + public static function errorDataProvider(): \Generator + { + // no errors, no message + yield [ + [], + null, + ]; + + // single error; JsonSchema 5.x + yield [ + [ + [ + 'property' => 'some-property', + 'pointer' => 'some-pointer', + 'message' => 'some-message', + 'constraint' => 'some-constraint', + 'context' => PHP_INT_MAX, + ], + ], + << 'some-property', + 'pointer' => 'some-pointer', + 'message' => 'some-message', + 'constraint' => ['name' => 'some-constraint', 'params' => []], + 'context' => PHP_INT_MAX, + ], + ], + << 'some-property2', + 'pointer' => 'some-pointer2', + 'message' => 'some-message', + 'constraint' => ['name' => 'some-constraint', 'params' => []], + 'context' => PHP_INT_MAX, + ], + [ + 'property' => 'some-property', + 'pointer' => 'some-pointer', + 'message' => 'some-message', + 'constraint' => ['name' => 'some-constraint', 'params' => []], + 'context' => PHP_INT_MAX, + ], + ], + << 'some-property[2]', + 'pointer' => 'some-pointer2', + 'message' => 'some-message', + 'constraint' => ['name' => 'some-constraint', 'params' => []], + 'context' => abs(PHP_INT_MAX), + ], + [ + 'property' => 'some-property[2]', + 'pointer' => 'some-pointer', + 'message' => 'some-message', + 'constraint' => ['name' => 'some-constraint', 'params' => []], + 'context' => PHP_INT_MAX, + ], + ], + <<