Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bbaa22b
test: round trip a complete document per OpenAPI version
AdamFiser Aug 3, 2026
981ea43
test: require the version validator to stay silent on a matching docu…
AdamFiser Aug 3, 2026
82e739c
test: require each complete document to exercise its version's additions
AdamFiser Aug 3, 2026
0738381
test: cover document-level 3.0 fields in the complete document
AdamFiser Aug 3, 2026
88a1ac0
test: cover every operation, parameter and response field of 3.0
AdamFiser Aug 3, 2026
a424ec4
test: cover every header field of 3.0
AdamFiser Aug 3, 2026
be7c113
test: cover every components section of 3.0
AdamFiser Aug 3, 2026
e6e4adc
test: make the complete 3.1 document a superset of the 3.0 one
AdamFiser Aug 3, 2026
b113a0f
test: keep the complete 3.1 document a superset of the 3.0 one
AdamFiser Aug 3, 2026
633e635
docs: state which OpenAPI versions are supported
AdamFiser Aug 3, 2026
6c57629
test: declare widely supported patch versions in the complete documents
AdamFiser Aug 3, 2026
c7c560a
test: use the OAS dialect as the jsonSchemaDialect value
AdamFiser Aug 3, 2026
e8ecad9
test: drop query-only traits from the header objects
AdamFiser Aug 3, 2026
1da92cd
test: point the links at operations the documents define
AdamFiser Aug 3, 2026
3f44dcd
test: make the 2020-12 keyword example coherent
AdamFiser Aug 3, 2026
d13c21a
refactor: keep the optional-since table private
AdamFiser Aug 3, 2026
f864b4f
test: stop the coverage table fold from hiding a version
AdamFiser Aug 3, 2026
7b6892e
fix: keep specification extensions in headers and security schemes
AdamFiser Aug 3, 2026
613dee3
test: exercise extensions on headers, schemes and flows
AdamFiser Aug 3, 2026
688680b
docs: state only what the version support test enforces
AdamFiser Aug 3, 2026
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
18 changes: 17 additions & 1 deletion .docs/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Contributte OpenApi

Pure PHP OpenAPI 3.0 implementation for Nette Framework.
Pure PHP OpenAPI implementation for Nette Framework, supporting 3.0 and 3.1.

## Content

- [Setup](#setup)
- [Supported versions](#supported-versions)
- [OpenAPI](#tracy)
- [Version validation](#version-validation)
- [Tracy](#tracy)
Expand Down Expand Up @@ -46,6 +47,21 @@ composer require contributte/openapi
- [ServerVariable.php](../src/Schema/ServerVariable.php)
- [Tag.php](../src/Schema/Tag.php)

## Supported versions

| OpenAPI | Support |
|---------|---------|
| 3.0 | yes |
| 3.1 | yes |

`tests/Cases/VersionSupportTest.php` backs both rows with a complete document per version -
`tests/Cases/Schema/examples/complete-3-0.yaml` and `complete-3-1.yaml` - each using every field
its version defines, except where the specification makes two fields mutually exclusive and only
one of them can appear. For each document the test requires that a `fromArray()`/`toArray()` round
trip loses nothing and that `VersionValidator` reports no problem. For the fields a version
introduced, it additionally requires the document to exercise them, so a later version cannot be
called supported while its additions go untested.

## Version validation

The schema classes accept any document, whatever version it declares. `VersionValidator`
Expand Down
18 changes: 18 additions & 0 deletions src/Schema/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Header
/** @var MediaType[]|null */
private ?array $content = null;

private ?VendorExtensions $vendorExtensions = null;

/**
* @param mixed[] $data
*/
Expand Down Expand Up @@ -62,6 +64,8 @@ public static function fromArray(array $data): Header
$header->setContent($key, MediaType::fromArray($contentData));
}

$header->setVendorExtensions(VendorExtensions::fromArray($data));

return $header;
}

Expand Down Expand Up @@ -116,6 +120,10 @@ public function toArray(): array
$data['content'] = array_map(static fn (MediaType $mediaType): array => $mediaType->toArray(), $this->content);
}

if ($this->vendorExtensions !== null) {
$data = array_merge($data, $this->vendorExtensions->toArray());
}

return $data;
}

Expand Down Expand Up @@ -177,4 +185,14 @@ public function setContent(string $type, MediaType $mediaType): void
$this->content[$type] = $mediaType;
}

public function getVendorExtensions(): ?VendorExtensions
{
return $this->vendorExtensions;
}

public function setVendorExtensions(?VendorExtensions $vendorExtensions): void
{
$this->vendorExtensions = $vendorExtensions;
}

}
21 changes: 20 additions & 1 deletion src/Schema/OAuthFlow.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class OAuthFlow
/** @var array<string, string> */
private array $scopes;

private ?VendorExtensions $vendorExtensions = null;

/**
* @param array<string, string> $scopes
*/
Expand All @@ -35,12 +37,15 @@ public function __construct(
*/
public static function fromArray(array $data): self
{
return new self(
$flow = new self(
$data['authorizationUrl'] ?? null,
$data['tokenUrl'] ?? null,
$data['refreshUrl'] ?? null,
$data['scopes'],
);
$flow->setVendorExtensions(VendorExtensions::fromArray($data));

return $flow;
}

/**
Expand All @@ -64,6 +69,10 @@ public function toArray(): array

$data['scopes'] = $this->scopes;

if ($this->vendorExtensions !== null) {
$data = array_merge($data, $this->vendorExtensions->toArray());
}

return $data;
}

Expand Down Expand Up @@ -113,4 +122,14 @@ public function setScopes(array $scopes): void
$this->scopes = $scopes;
}

public function getVendorExtensions(): ?VendorExtensions
{
return $this->vendorExtensions;
}

public function setVendorExtensions(?VendorExtensions $vendorExtensions): void
{
$this->vendorExtensions = $vendorExtensions;
}

}
17 changes: 17 additions & 0 deletions src/Schema/SecurityScheme.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ class SecurityScheme

private ?string $openIdConnectUrl = null;

private ?VendorExtensions $vendorExtensions = null;

public function __construct(string $type)
{
$this->setType($type);
Expand All @@ -79,6 +81,7 @@ public static function fromArray(array $data): SecurityScheme
$securityScheme->setBearerFormat($data['bearerFormat'] ?? null);
$securityScheme->setFlows(array_map(static fn (array $flow): OAuthFlow => OAuthFlow::fromArray($flow), $data['flows'] ?? []));
$securityScheme->setOpenIdConnectUrl($data['openIdConnectUrl'] ?? null);
$securityScheme->setVendorExtensions(VendorExtensions::fromArray($data));

return $securityScheme;
}
Expand Down Expand Up @@ -119,6 +122,10 @@ public function toArray(): array
$data['openIdConnectUrl'] = $this->openIdConnectUrl;
}

if ($this->vendorExtensions !== null) {
$data = array_merge($data, $this->vendorExtensions->toArray());
}

return $data;
}

Expand Down Expand Up @@ -260,6 +267,16 @@ public function setOpenIdConnectUrl(?string $openIdConnectUrl): void
$this->openIdConnectUrl = $openIdConnectUrl;
}

public function getVendorExtensions(): ?VendorExtensions
{
return $this->vendorExtensions;
}

public function setVendorExtensions(?VendorExtensions $vendorExtensions): void
{
$this->vendorExtensions = $vendorExtensions;
}

private static function validateFlow(string $flowType, OAuthFlow $flow): void
{
$needsAuthorizationUrl = in_array($flowType, [
Expand Down
4 changes: 2 additions & 2 deletions src/Validator/VersionValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class VersionValidator
* Fields and the version that introduced them. A "*" segment matches any key
* of a map.
*/
private const FIELD_INTRODUCED_IN = [
public const FIELD_INTRODUCED_IN = [
'jsonSchemaDialect' => Version::V3_1,
'webhooks' => Version::V3_1,
'info.summary' => Version::V3_1,
Expand All @@ -34,7 +34,7 @@ class VersionValidator
* Field values and the version that introduced them, as
* "document path => [value => version]".
*/
private const VALUE_INTRODUCED_IN = [
public const VALUE_INTRODUCED_IN = [
'components.securitySchemes.*.type' => [
SecurityScheme::TYPE_MUTUAL_TLS => Version::V3_1,
],
Expand Down
13 changes: 13 additions & 0 deletions tests/Cases/Schema/HeaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ public function testContent(): void
Assert::same($expectedData, Header::fromArray($expectedData)->toArray());
}

public function testVendorExtensions(): void
{
$expectedData = [
'description' => 'The number of allowed requests in the current period',
'x-internal' => true,
];

$header = Header::fromArray($expectedData);

Assert::same(true, $header->getVendorExtensions()?->getExtension('x-internal'));
Assert::same($expectedData, $header->toArray());
}

}

(new HeaderTest())->run();
14 changes: 14 additions & 0 deletions tests/Cases/Schema/OAuthFlowTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@ public function testClientCredentialsFlowWithoutAuthorizationUrl(): void
Assert::same($data, $flow->toArray());
}

public function testVendorExtensions(): void
{
$expectedData = [
'authorizationUrl' => 'https://example.com/authorization',
'scopes' => ['read' => 'Read access'],
'x-internal' => true,
];

$flow = OAuthFlow::fromArray($expectedData);

Assert::same(true, $flow->getVendorExtensions()?->getExtension('x-internal'));
Assert::same($expectedData, $flow->toArray());
}

/**
* The OpenAPI Specification marks `scopes` as REQUIRED on the OAuth Flow Object (the map MAY
* be empty, but the key MUST be present). fromArray() must not silently manufacture it - a
Expand Down
15 changes: 15 additions & 0 deletions tests/Cases/Schema/SecuritySchemeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ public function testOptional(): void
Assert::same($expected, SecurityScheme::fromArray($array)->toArray());
}

public function testVendorExtensions(): void
{
$expectedData = [
'type' => SecurityScheme::TYPE_API_KEY,
'name' => 'api_key',
'in' => SecurityScheme::IN_HEADER,
'x-internal' => true,
];

$securityScheme = SecurityScheme::fromArray($expectedData);

Assert::same(true, $securityScheme->getVendorExtensions()?->getExtension('x-internal'));
Assert::same($expectedData, $securityScheme->toArray());
}

public function testInvalidType(): void
{
Assert::exception(static function (): void {
Expand Down
Loading