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
2 changes: 1 addition & 1 deletion .github/actions/setup-php-composer/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ inputs:
php-version:
description: 'PHP version to use'
required: false
default: '8.3'
default: '8.4'
coverage:
description: 'Coverage driver to use (none, xdebug, pcov)'
required: false
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
php: [8.3, 8.4, 8.5]
php: [8.4, 8.5]
stability: [prefer-lowest, prefer-stable]

name: PHP${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A modern, fluent builder for [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0)

## Requirements

- PHP 8.3+
- PHP 8.4+

## Installation

Expand Down
13 changes: 8 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,22 @@
}
],
"require": {
"php": "^8.3",
"php": "^8.4",
"ext-json": "*",
"cortexphp/json-schema": "^1.1",
"opis/json-schema": "^2.4"
},
"require-dev": {
"pestphp/pest": "^4.1.4",
"pestphp/pest-plugin-type-coverage": "^4.0.3",
"pestphp/pest": "^5.0",
"pestphp/pest-plugin-mutate": "^5.0.1",
"pestphp/pest-plugin-phpstan": "^5.0",
"pestphp/pest-plugin-rector": "^5.0",
"pestphp/pest-plugin-type-coverage": "^5.0",
"phpstan/phpstan": "^2.1.32",
"phpstan/phpstan-strict-rules": "^2.0",
"rector/rector": "^2.2",
"symfony/yaml": "^6.0 || ^7.0",
"symplify/easy-coding-standard": "13.2.3"
"symplify/easy-coding-standard": "~13.1.0"
},
"suggest": {
"symfony/yaml": "Required for OpenApi::toYaml() output (^6.0 || ^7.0)."
Expand All @@ -51,7 +54,7 @@
]
},
"scripts": {
"test": "pest --no-coverage",
"test": "pest --parallel",
"ecs": "ecs check --fix",
"ecs:check": "ecs check",
"rector": "rector process",
Expand Down
2 changes: 1 addition & 1 deletion docs/openapi/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ icon: 'terminal'

## Requirements

- PHP 8.3+
- PHP 8.4+

## Installation

Expand Down
4 changes: 2 additions & 2 deletions docs/openapi/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ OpenAPI 3.1 aligns fully with **JSON Schema 2020-12**, which means every schema
title="Type Safety"
icon="shield"
>
Built with PHP 8.3+ strict types, backed enums, and named constructors. PHPStan level 10 clean with 100% type coverage.
Built with PHP 8.4+ strict types, backed enums, and named constructors. PHPStan level 10 clean with 100% type coverage.
</Card>
</CardGroup>

Expand Down Expand Up @@ -130,7 +130,7 @@ echo $doc->toJson(JSON_PRETTY_PRINT);
```

<Note>
This package requires PHP 8.3 or higher. `symfony/yaml` is an optional runtime dependency — only needed if you call `toYaml()`.
This package requires PHP 8.4 or higher. `symfony/yaml` is an optional runtime dependency — only needed if you call `toYaml()`.
</Note>

## How It Relates to cortexphp/json-schema
Expand Down
2 changes: 1 addition & 1 deletion ecs.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
cleanCode: true,
)
->withPhpCsFixerSets(
php83Migration: true,
php84Migration: true,
)
->withRules([
NotOperatorWithSuccessorSpaceFixer::class,
Expand Down
4 changes: 2 additions & 2 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
- vendor/pestphp/pest-plugin-phpstan/extension.neon

parameters:
level: 10
paths:
- src
stubFiles:
- tests/phpstan/pest-expectations.stub
- tests
tmpDir: .phpstan-cache
checkBenevolentUnionTypes: true
8 changes: 7 additions & 1 deletion rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use Rector\Config\RectorConfig;
use Pest\Rector\Set\PestSetList;
use Rector\CodeQuality\Rector\Catch_\ThrowWithPreviousExceptionRector;

return RectorConfig::configure()
Expand All @@ -15,7 +16,12 @@
importDocBlockNames: false,
removeUnusedImports: true,
)
->withPhpSets()
->withPhpSets(
php84: true,
)
->withSets([
PestSetList::CODING_STYLE,
])
->withPreparedSets(
deadCode: true,
codeQuality: true,
Expand Down
6 changes: 3 additions & 3 deletions src/Concerns/BuildsArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ protected function buildArray(array $fields, array $alwaysInclude = []): array
}

if ($this instanceof HasExtensionsInterface) {
foreach ($this->getExtensions() as $extensionKey => $extensionValue) {
$output[$extensionKey] = $extensionValue;
foreach ($this->getExtensions() as $extensionKey => $extensionValue) { // @phpstan-ignore foreach.nonIterable
$output[$extensionKey] = $extensionValue; // @phpstan-ignore offsetAccess.invalidOffset
}
}

return $output;
return $output; // @phpstan-ignore return.type
}

private function unwrapValue(mixed $value): mixed
Expand Down
2 changes: 1 addition & 1 deletion src/OpenApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ public function validate(): void
return;
}

$formatted = (new ErrorFormatter())->format($error);
$formatted = new ErrorFormatter()->format($error);

throw new ValidationException(
'OpenAPI document failed meta-schema validation: ' . json_encode($formatted, JSON_UNESCAPED_SLASHES),
Expand Down
89 changes: 48 additions & 41 deletions tests/Expectations.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,59 @@
declare(strict_types=1);

use Cortex\OpenApi\OpenApi;
use PHPUnit\Framework\Assert;
use Cortex\OpenApi\Exceptions\ValidationException;

/**
* Custom Pest expectations for OpenAPI meta-schema validation.
* Method signatures for IDE/static analysis: tests/phpstan/pest-expectations.stub
*
* @return array<mixed>
*/
function expectArray(mixed $value): array
{
expect($value)->toBeArray();

if (! is_array($value)) {
Assert::fail('Expected array.');
}

return $value;
}

/**
* @param array<int, int|string> $keys
*/
function keyIndex(array $keys, string $key): int
{
$index = array_search($key, $keys, true);
expect($index)->toBeInt();

if (! is_int($index)) {
Assert::fail("Expected key [{$key}] to exist.");
}

return $index;
}

function assertOpenApiValidationPasses(OpenApi $openApi): void
{
try {
$openApi->validate();
} catch (ValidationException $validationException) {
Assert::fail(
'Expected OpenAPI document to pass validation: ' . $validationException->getMessage(),
);
}

Assert::assertInstanceOf(OpenApi::class, $openApi);
}

/**
* @param array<string, list<string>> $expectedErrors
*/
function assertOpenApiValidationErrors(OpenApi $openApi, array $expectedErrors): void
{
try {
$openApi->validate();
test()->fail('Expected ValidationException');
Assert::fail('Expected ValidationException');
} catch (ValidationException $validationException) {
foreach ($expectedErrors as $pointer => $messages) {
expect($validationException->errors())->toHaveKey($pointer);
Expand All @@ -26,42 +66,9 @@ function assertOpenApiValidationErrors(OpenApi $openApi, array $expectedErrors):
}
}

expect()->extend('toFailOpenApiValidation', function (array $expectedErrors): mixed {
if (! $this->value instanceof OpenApi) {
test()->fail('Expected an OpenApi instance.');
}

assertOpenApiValidationErrors($this->value, $expectedErrors);

return $this;
});

expect()->extend('toFailOpenApiValidationAt', function (string $pointer, string $message): mixed {
if (! $this->value instanceof OpenApi) {
test()->fail('Expected an OpenApi instance.');
}

assertOpenApiValidationErrors($this->value, [
function assertOpenApiValidationFailsAt(OpenApi $openApi, string $pointer, string $message): void
{
assertOpenApiValidationErrors($openApi, [
$pointer => [$message],
]);

return $this;
});

expect()->extend('toPassOpenApiValidation', function (): mixed {
if (! $this->value instanceof OpenApi) {
test()->fail('Expected an OpenApi instance.');
}

try {
$this->value->validate();
} catch (ValidationException $validationException) {
test()->fail(
'Expected OpenAPI document to pass validation: ' . $validationException->getMessage(),
);
}

expect($this->value)->toBeInstanceOf(OpenApi::class);

return $this;
});
}
4 changes: 3 additions & 1 deletion tests/Pest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@

namespace Cortex\OpenApi\Tests;

uses(TestCase::class)->in('Unit');
pest()
->extend(TestCase::class)
->in('Unit');
30 changes: 15 additions & 15 deletions tests/Unit/Concerns/BuildsArrayTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public function assemble(array $fields): array
}

it('drops null fields', function (): void {
$out = (new BuildsArrayFixture())->assemble([
$out = new BuildsArrayFixture()->assemble([
'title' => 'X',
'version' => null,
]);
Expand All @@ -66,7 +66,7 @@ public function assemble(array $fields): array
});

it('drops empty arrays', function (): void {
$out = (new BuildsArrayFixture())->assemble([
$out = new BuildsArrayFixture()->assemble([
'title' => 'X',
'tags' => [],
]);
Expand All @@ -77,7 +77,7 @@ public function assemble(array $fields): array
});

it('unwraps a Serializable child', function (): void {
$out = (new BuildsArrayFixture())->assemble([
$out = new BuildsArrayFixture()->assemble([
'info' => new BuildsArraySerializableFixture(),
]);

Expand All @@ -89,7 +89,7 @@ public function assemble(array $fields): array
});

it('unwraps a list of Serializable children preserving list semantics', function (): void {
$out = (new BuildsArrayFixture())->assemble([
$out = new BuildsArrayFixture()->assemble([
'tags' => [
new BuildsArraySerializableFixture(),
new BuildsArraySerializableFixture(),
Expand All @@ -109,7 +109,7 @@ public function assemble(array $fields): array
});

it('unwraps an associative array of Serializable children preserving keys', function (): void {
$out = (new BuildsArrayFixture())->assemble([
$out = new BuildsArrayFixture()->assemble([
'paths' => [
'/users' => new BuildsArraySerializableFixture(),
'/pets' => new BuildsArraySerializableFixture(),
Expand Down Expand Up @@ -143,7 +143,7 @@ public function assemble(array $fields): array
});

it('preserves explicit false values', function (): void {
$out = (new BuildsArrayFixture())->assemble([
$out = new BuildsArrayFixture()->assemble([
'deprecated' => false,
]);

Expand All @@ -153,7 +153,7 @@ public function assemble(array $fields): array
});

it('preserves explicit zero values', function (): void {
$out = (new BuildsArrayFixture())->assemble([
$out = new BuildsArrayFixture()->assemble([
'minimum' => 0,
]);

Expand Down Expand Up @@ -181,10 +181,10 @@ public function assemble(array $fields): array

// Reference: stand-alone toArray() would include both $schema URI and title.
$standalone = $stringSchema->toArray();
expect($standalone)->toHaveKey('$schema');
expect($standalone)->toHaveKey('title', 'IgnoredTitle');
expect($standalone)->toHaveKey('$schema')
->toHaveKey('title', 'IgnoredTitle');

$out = (new BuildsArrayFixture())->assemble([
$out = new BuildsArrayFixture()->assemble([
'schema' => $stringSchema,
]);

Expand All @@ -202,7 +202,7 @@ public function assemble(array $fields): array
expect($objectSchema->getTitle())->toBe('consults')
->and($objectSchema->getInitialTitle())->toBe('Consult');

$out = (new BuildsArrayFixture())->assemble([
$out = new BuildsArrayFixture()->assemble([
'schema' => $objectSchema,
]);

Expand All @@ -211,8 +211,8 @@ public function assemble(array $fields): array
'type' => 'object',
'title' => 'consults',
],
]);
expect($out['schema'])->not->toHaveKey('$schema');
])
->and($out['schema'])->not->toHaveKey('$schema');
});

it('keeps a JsonSchema title set on a schema that had no constructor title', function (): void {
Expand All @@ -221,7 +221,7 @@ public function assemble(array $fields): array
expect($stringSchema->getTitle())->toBe('IsoDateTime')
->and($stringSchema->getInitialTitle())->toBeNull();

$out = (new BuildsArrayFixture())->assemble([
$out = new BuildsArrayFixture()->assemble([
'schema' => $stringSchema,
]);

Expand All @@ -236,7 +236,7 @@ public function assemble(array $fields): array
it('strips a JsonSchema title that merely restates the constructor argument', function (): void {
$stringSchema = Schema::string('IsoDateTime')->title('IsoDateTime');

$out = (new BuildsArrayFixture())->assemble([
$out = new BuildsArrayFixture()->assemble([
'schema' => $stringSchema,
]);

Expand Down
Loading