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
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
[![psalm-level](https://shepherd.dev/github/duyler/openapi/level.svg)](https://shepherd.dev/github/duyler/openapi)
![PHP Version](https://img.shields.io/packagist/dependency-v/duyler/openapi/php?version=dev-main)

OpenAPI 3.1 validator for PHP 8.4+
OpenAPI 3.2 validator for PHP 8.4+

## Features

- **Full OpenAPI 3.1 Support** - Complete implementation of OpenAPI 3.1 specification
- **Full OpenAPI 3.2 Support** - Complete implementation of OpenAPI 3.2 specification
- **JSON Schema Validation** - Full JSON Schema draft 2020-12 validation with 25+ validators
- **PSR-7 Integration** - Works with any PSR-7 HTTP message implementation
- **Request Validation** - Validate path parameters, query parameters, headers, cookies, and request body
Expand Down Expand Up @@ -229,7 +229,7 @@ Validate polymorphic schemas with discriminators:

```php
$yaml = <<<YAML
openapi: 3.1.0
openapi: 3.2.0
info:
title: Pet Store API
version: 1.0.0
Expand Down Expand Up @@ -667,14 +667,14 @@ $validator = OpenApiValidatorBuilder::create()

### Key Differences

| Feature | league/openapi-psr7-validator | duyler/openapi |
|---------|------------------------------|----------------|
| PHP Version | PHP 7.4+ | PHP 8.4+ |
| OpenAPI Version | 3.0 | 3.1 |
| JSON Schema | Draft 7 | Draft 2020-12 |
| Feature | league/openapi-psr7-validator | duyler/openapi |
|---------|------------------------------|----------------------------|
| PHP Version | PHP 7.4+ | PHP 8.4+ |
| OpenAPI Version | 3.0 | 3.0, 3.1, 3.2 |
| JSON Schema | Draft 7 | Draft 2020-12 |
| Builder Pattern | Fluent builder | Fluent builder (immutable) |
| Type Coercion | Enabled by default | Opt-in |
| Error Formatting | Basic | Multiple formatters |
| Type Coercion | Enabled by default | Opt-in |
| Error Formatting | Basic | Multiple formatters |

### Migration Examples

Expand Down
26 changes: 4 additions & 22 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,20 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Psalm Baseline Documentation

ClassMustBeFinal suppressions for readonly classes:
PHP 8.2+ readonly classes are implicitly final and cannot be extended.
Psalm's ClassMustBeFinal rule does not account for this language feature.
These suppressions are necessary until Psalm is updated.

MixedAssignment suppressions:
- Schema.php ($default, $const, $example): These represent JSON Schema values
which can be any valid JSON type including nested objects. Using `mixed` is
correct; union types would be incomplete.
- OpenApiBuilder.php: Parser iterates over decoded JSON/YAML which is mixed
by nature before type narrowing via assertions.
- Coercion/Validator classes: Dynamic type coercion requires mixed handling.
-->
<files psalm-version="6.14.3@d0b040a91f280f071c1abcb1b77ce3822058725a">
<file src="src/Cache/SchemaCache.php">
<ClassMustBeFinal>
Expand Down Expand Up @@ -108,6 +92,8 @@ MixedAssignment suppressions:
<code><![CDATA[Example]]></code>
</ClassMustBeFinal>
<MixedAssignment>
<code><![CDATA[$data['dataValue']]]></code>
<code><![CDATA[$data['serializedValue']]]></code>
<code><![CDATA[$data['value']]]></code>
</MixedAssignment>
</file>
Expand Down Expand Up @@ -245,9 +231,6 @@ MixedAssignment suppressions:
</ClassMustBeFinal>
</file>
<file src="src/Schema/Parser/JsonParser.php">
<ClassMustBeFinal>
<code><![CDATA[JsonParser]]></code>
</ClassMustBeFinal>
</file>
<file src="src/Schema/Parser/OpenApiBuilder.php">
<MixedAssignment>
Expand All @@ -259,6 +242,8 @@ MixedAssignment suppressions:
<code><![CDATA[$header]]></code>
<code><![CDATA[$link]]></code>
<code><![CDATA[$link]]></code>
<code><![CDATA[$mediaType]]></code>
<code><![CDATA[$operationData]]></code>
<code><![CDATA[$parameter]]></code>
<code><![CDATA[$pathItem]]></code>
<code><![CDATA[$pathItem]]></code>
Expand All @@ -282,9 +267,6 @@ MixedAssignment suppressions:
</MixedAssignment>
</file>
<file src="src/Schema/Parser/YamlParser.php">
<ClassMustBeFinal>
<code><![CDATA[YamlParser]]></code>
</ClassMustBeFinal>
</file>
<file src="src/Validator/Error/Breadcrumb.php">
<ClassMustBeFinal>
Expand Down
2 changes: 1 addition & 1 deletion src/Compiler/ValidatorCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ private function generateArrayCheck(Schema $schema): string
$code .= " }\n\n";
}

if (null !== $schema->uniqueItems && true === $schema->uniqueItems) {
if (null !== $schema->uniqueItems && $schema->uniqueItems) {
$code .= " if (count(\$data) !== count(array_unique(\$data, SORT_REGULAR))) {\n";
$code .= " throw new \\RuntimeException('Array items must be unique');\n";
$code .= " }\n\n";
Expand Down
9 changes: 9 additions & 0 deletions src/Exception/RefResolutionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Duyler\OpenApi\Exception;

use Exception;

final class RefResolutionException extends Exception {}
26 changes: 16 additions & 10 deletions src/Schema/Model/Components.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* @param array<string, Link>|null $links
* @param array<string, Callbacks>|null $callbacks
* @param array<string, PathItem>|null $pathItems
* @param array<string, MediaType>|null $mediaTypes
*/
public function __construct(
public ?array $schemas = null,
Expand All @@ -32,53 +33,58 @@ public function __construct(
public ?array $links = null,
public ?array $callbacks = null,
public ?array $pathItems = null,
public ?array $mediaTypes = null,
) {}

#[Override]
public function jsonSerialize(): array
{
$data = [];

if ($this->schemas !== null) {
if (null !== $this->schemas) {
$data['schemas'] = $this->schemas;
}

if ($this->responses !== null) {
if (null !== $this->responses) {
$data['responses'] = $this->responses;
}

if ($this->parameters !== null) {
if (null !== $this->parameters) {
$data['parameters'] = $this->parameters;
}

if ($this->examples !== null) {
if (null !== $this->examples) {
$data['examples'] = $this->examples;
}

if ($this->requestBodies !== null) {
if (null !== $this->requestBodies) {
$data['requestBodies'] = $this->requestBodies;
}

if ($this->headers !== null) {
if (null !== $this->headers) {
$data['headers'] = $this->headers;
}

if ($this->securitySchemes !== null) {
if (null !== $this->securitySchemes) {
$data['securitySchemes'] = $this->securitySchemes;
}

if ($this->links !== null) {
if (null !== $this->links) {
$data['links'] = $this->links;
}

if ($this->callbacks !== null) {
if (null !== $this->callbacks) {
$data['callbacks'] = $this->callbacks;
}

if ($this->pathItems !== null) {
if (null !== $this->pathItems) {
$data['pathItems'] = $this->pathItems;
}

if (null !== $this->mediaTypes) {
$data['mediaTypes'] = $this->mediaTypes;
}

return $data;
}
}
6 changes: 3 additions & 3 deletions src/Schema/Model/Contact.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ public function jsonSerialize(): array
{
$data = [];

if ($this->name !== null) {
if (null !== $this->name) {
$data['name'] = $this->name;
}

if ($this->url !== null) {
if (null !== $this->url) {
$data['url'] = $this->url;
}

if ($this->email !== null) {
if (null !== $this->email) {
$data['email'] = $this->email;
}

Expand Down
17 changes: 12 additions & 5 deletions src/Schema/Model/Discriminator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,28 @@
* @param array<string, string> $mapping
*/
public function __construct(
public string $propertyName,
public ?string $propertyName = null,
public ?array $mapping = null,
public ?string $defaultMapping = null,
) {}

#[Override]
public function jsonSerialize(): array
{
$data = [
'propertyName' => $this->propertyName,
];
$data = [];

if ($this->mapping !== null) {
if (null !== $this->propertyName) {
$data['propertyName'] = $this->propertyName;
}

if (null !== $this->mapping) {
$data['mapping'] = $this->mapping;
}

if (null !== $this->defaultMapping) {
$data['defaultMapping'] = $this->defaultMapping;
}

return $data;
}
}
59 changes: 59 additions & 0 deletions src/Schema/Model/Encoding.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Duyler\OpenApi\Schema\Model;

use JsonSerializable;
use Override;

final readonly class Encoding implements JsonSerializable
{
/**
* @param array<string, Encoding>|null $encoding
* @param array<int, Encoding>|null $prefixEncoding
*/
public function __construct(
public ?string $contentType = null,
public ?Headers $headers = null,
public ?string $style = null,
public ?bool $explode = null,
public ?bool $allowReserved = null,
public ?array $encoding = null,
public ?array $prefixEncoding = null,
public ?Encoding $itemEncoding = null,
) {}

#[Override]
public function jsonSerialize(): array
{
$result = [];

if (null !== $this->contentType) {
$result['contentType'] = $this->contentType;
}
if (null !== $this->headers) {
$result['headers'] = $this->headers;
}
if (null !== $this->style) {
$result['style'] = $this->style;
}
if (null !== $this->explode) {
$result['explode'] = $this->explode;
}
if (null !== $this->allowReserved) {
$result['allowReserved'] = $this->allowReserved;
}
if (null !== $this->encoding) {
$result['encoding'] = $this->encoding;
}
if (null !== $this->prefixEncoding) {
$result['prefixEncoding'] = $this->prefixEncoding;
}
if (null !== $this->itemEncoding) {
$result['itemEncoding'] = $this->itemEncoding;
}

return $result;
}
}
23 changes: 19 additions & 4 deletions src/Schema/Model/Example.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,45 @@ public function __construct(
public ?string $summary = null,
public ?string $description = null,
public mixed $value = null,
public mixed $dataValue = null,
public mixed $serializedValue = null,
public ?string $externalValue = null,
public ?string $serializedExample = null,
) {}

#[Override]
public function jsonSerialize(): array
{
$data = [];

if ($this->summary !== null) {
if (null !== $this->summary) {
$data['summary'] = $this->summary;
}

if ($this->description !== null) {
if (null !== $this->description) {
$data['description'] = $this->description;
}

if ($this->value !== null) {
if (null !== $this->value) {
$data['value'] = $this->value;
}

if ($this->externalValue !== null) {
if (null !== $this->dataValue) {
$data['dataValue'] = $this->dataValue;
}

if (null !== $this->serializedValue) {
$data['serializedValue'] = $this->serializedValue;
}

if (null !== $this->externalValue) {
$data['externalValue'] = $this->externalValue;
}

if (null !== $this->serializedExample) {
$data['serializedExample'] = $this->serializedExample;
}

return $data;
}
}
2 changes: 1 addition & 1 deletion src/Schema/Model/ExternalDocs.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function jsonSerialize(): array
'url' => $this->url,
];

if ($this->description !== null) {
if (null !== $this->description) {
$data['description'] = $this->description;
}

Expand Down
Loading