From bbaa22b0ddaa6bd2b309783b5c4d749b489657e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 07:24:08 +0200 Subject: [PATCH 01/20] test: round trip a complete document per OpenAPI version --- tests/Cases/Schema/examples/complete-3-0.yaml | 10 ++ tests/Cases/Schema/examples/complete-3-1.yaml | 10 ++ tests/Cases/VersionSupportTest.php | 95 +++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 tests/Cases/Schema/examples/complete-3-0.yaml create mode 100644 tests/Cases/Schema/examples/complete-3-1.yaml create mode 100644 tests/Cases/VersionSupportTest.php diff --git a/tests/Cases/Schema/examples/complete-3-0.yaml b/tests/Cases/Schema/examples/complete-3-0.yaml new file mode 100644 index 0000000..6adba96 --- /dev/null +++ b/tests/Cases/Schema/examples/complete-3-0.yaml @@ -0,0 +1,10 @@ +openapi: "3.0.4" +info: + title: Complete 3.0 document + version: 1.0.0 +paths: + /pets: + get: + responses: + '200': + description: A list of pets diff --git a/tests/Cases/Schema/examples/complete-3-1.yaml b/tests/Cases/Schema/examples/complete-3-1.yaml new file mode 100644 index 0000000..9b68cb2 --- /dev/null +++ b/tests/Cases/Schema/examples/complete-3-1.yaml @@ -0,0 +1,10 @@ +openapi: "3.1.1" +info: + title: Complete 3.1 document + version: 1.0.0 +paths: + /pets: + get: + responses: + '200': + description: A list of pets diff --git a/tests/Cases/VersionSupportTest.php b/tests/Cases/VersionSupportTest.php new file mode 100644 index 0000000..3cd36e4 --- /dev/null +++ b/tests/Cases/VersionSupportTest.php @@ -0,0 +1,95 @@ + file name". + private const COMPLETE_DOCUMENTS = [ + Version::V3_0 => 'complete-3-0.yaml', + Version::V3_1 => 'complete-3-1.yaml', + ]; + + /** + * @return array + */ + public function provideCompleteDocuments(): array + { + $rows = []; + + foreach (self::COMPLETE_DOCUMENTS as $version => $file) { + $rows[$version] = [$version, $file]; + } + + return $rows; + } + + /** + * @dataProvider provideCompleteDocuments + */ + public function testCompleteDocument(string $version, string $file): void + { + $rawData = Yaml::parseFile(self::DOCUMENT_DIRECTORY . $file); + + $openApi = OpenApi::fromArray($rawData); + + self::assertSameDataStructure($rawData, $openApi->toArray(), $version); + } + + /** + * @param mixed[] $expected + * @param mixed[] $actual + */ + private static function assertSameDataStructure(array $expected, array $actual, string $version): void + { + Assert::same( + self::recursiveSort($expected), + self::recursiveSort($actual), + sprintf('document of version %s survives a round trip', $version) + ); + } + + /** + * Key order carries no meaning in an OpenAPI document, so both sides are sorted before + * they are compared. + * + * @param mixed[] $data + * @return mixed[] + */ + private static function recursiveSort(array $data): array + { + foreach ($data as $key => $value) { + if (!is_array($value)) { + continue; + } + + $data[$key] = self::recursiveSort($value); + } + + unset($value); + ksort($data); + + return $data; + } + +} + +(new VersionSupportTest())->run(); From 981ea435d4be4864fb35abdfaf4a3c027ea4fdcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 07:30:33 +0200 Subject: [PATCH 02/20] test: require the version validator to stay silent on a matching document --- tests/Cases/VersionSupportTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Cases/VersionSupportTest.php b/tests/Cases/VersionSupportTest.php index 3cd36e4..d33fe15 100644 --- a/tests/Cases/VersionSupportTest.php +++ b/tests/Cases/VersionSupportTest.php @@ -5,6 +5,7 @@ require_once __DIR__ . '/../bootstrap.php'; use Contributte\OpenApi\Schema\OpenApi; +use Contributte\OpenApi\Validator\VersionValidator; use Contributte\OpenApi\Version; use Symfony\Component\Yaml\Yaml; use Tester\Assert; @@ -52,6 +53,12 @@ public function testCompleteDocument(string $version, string $file): void $openApi = OpenApi::fromArray($rawData); self::assertSameDataStructure($rawData, $openApi->toArray(), $version); + + Assert::same( + [], + array_map(strval(...), (new VersionValidator())->validate($openApi)), + sprintf('document of version %s raises no version problem', $version) + ); } /** From 82e739c58f7b5e8daf74e8ef97271bbb97537452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 07:42:31 +0200 Subject: [PATCH 03/20] test: require each complete document to exercise its version's additions --- src/Validator/VersionValidator.php | 6 +- tests/Cases/Schema/examples/complete-3-1.yaml | 40 ++++++++++ tests/Cases/VersionSupportTest.php | 74 +++++++++++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/src/Validator/VersionValidator.php b/src/Validator/VersionValidator.php index 8aaf1b3..50d671e 100644 --- a/src/Validator/VersionValidator.php +++ b/src/Validator/VersionValidator.php @@ -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, @@ -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, ], @@ -43,7 +43,7 @@ class VersionValidator /** * Required fields and the version that made them optional. */ - private const FIELD_OPTIONAL_SINCE = [ + public const FIELD_OPTIONAL_SINCE = [ 'paths' => Version::V3_1, ]; diff --git a/tests/Cases/Schema/examples/complete-3-1.yaml b/tests/Cases/Schema/examples/complete-3-1.yaml index 9b68cb2..ab5173d 100644 --- a/tests/Cases/Schema/examples/complete-3-1.yaml +++ b/tests/Cases/Schema/examples/complete-3-1.yaml @@ -1,10 +1,50 @@ openapi: "3.1.1" +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema info: title: Complete 3.1 document + summary: Uses every field OpenAPI 3.1 defines. version: 1.0.0 + license: + name: MIT + identifier: MIT paths: /pets: get: responses: '200': description: A list of pets + content: + application/json: + schema: + type: [array, "null"] + prefixItems: + - type: string + unevaluatedProperties: false + contentMediaType: application/json +webhooks: + petCreated: + post: + requestBody: + content: + application/json: + schema: + type: object + const: null + responses: + '200': + description: Acknowledged + petDeleted: + $ref: '#/components/pathItems/petEvent' + summary: Reference summary + description: Reference description +components: + pathItems: + petEvent: + post: + responses: + '200': + description: Acknowledged + securitySchemes: + mtls: + type: mutualTLS + description: Client certificate authentication diff --git a/tests/Cases/VersionSupportTest.php b/tests/Cases/VersionSupportTest.php index d33fe15..a662817 100644 --- a/tests/Cases/VersionSupportTest.php +++ b/tests/Cases/VersionSupportTest.php @@ -5,6 +5,7 @@ require_once __DIR__ . '/../bootstrap.php'; use Contributte\OpenApi\Schema\OpenApi; +use Contributte\OpenApi\Validator\Problem; use Contributte\OpenApi\Validator\VersionValidator; use Contributte\OpenApi\Version; use Symfony\Component\Yaml\Yaml; @@ -59,6 +60,37 @@ public function testCompleteDocument(string $version, string $file): void array_map(strval(...), (new VersionValidator())->validate($openApi)), sprintf('document of version %s raises no version problem', $version) ); + + $expectedPaths = self::expectedPaths($version); + + if ($expectedPaths === []) { + return; + } + + $rawData['openapi'] = Version::SUPPORTED[0]; + $reported = array_map( + static fn (Problem $problem): string => $problem->getPath(), + (new VersionValidator())->validate(OpenApi::fromArray($rawData)) + ); + + $uncovered = array_values(array_filter( + $expectedPaths, + static function (string $pattern) use ($reported): bool { + foreach ($reported as $path) { + if (self::matchesPath($pattern, $path)) { + return false; + } + } + + return true; + } + )); + + Assert::same( + [], + $uncovered, + sprintf('document of version %s exercises every field that version introduced', $version) + ); } /** @@ -97,6 +129,48 @@ private static function recursiveSort(array $data): array return $data; } + /** + * Rule paths a document of this version must exercise: everything introduced after the + * oldest supported version, up to and including this one. The validator's tables are the + * list of what the library knows about versions, so they double as the coverage list. + * + * @return string[] + */ + private static function expectedPaths(string $version): array + { + $oldest = Version::SUPPORTED[0]; + $paths = []; + + $introducedIn = VersionValidator::FIELD_INTRODUCED_IN; + + foreach (VersionValidator::VALUE_INTRODUCED_IN as $path => $values) { + foreach ($values as $introduced) { + $introducedIn[$path] = $introduced; + } + } + + foreach ($introducedIn as $path => $introduced) { + if (Version::isBefore($oldest, $introduced) && !Version::isBefore($version, $introduced)) { + $paths[] = $path; + } + } + + sort($paths); + + return $paths; + } + + /** + * Whether a concrete document path matches a rule path, whose "*" segment stands for any + * single key of a map. + */ + private static function matchesPath(string $pattern, string $path): bool + { + $regex = '#^' . str_replace('\*', '[^.]+', preg_quote($pattern, '#')) . '$#'; + + return preg_match($regex, $path) === 1; + } + } (new VersionSupportTest())->run(); From 07383812d13e61793b0f07599366207f953d158c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 07:46:59 +0200 Subject: [PATCH 04/20] test: cover document-level 3.0 fields in the complete document --- tests/Cases/Schema/examples/complete-3-0.yaml | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/Cases/Schema/examples/complete-3-0.yaml b/tests/Cases/Schema/examples/complete-3-0.yaml index 6adba96..7d33d01 100644 --- a/tests/Cases/Schema/examples/complete-3-0.yaml +++ b/tests/Cases/Schema/examples/complete-3-0.yaml @@ -1,7 +1,43 @@ openapi: "3.0.4" info: title: Complete 3.0 document + description: Uses every field OpenAPI 3.0 defines. + termsOfService: https://example.com/terms + contact: + name: API Support + url: https://example.com/support + email: support@example.com + license: + name: MIT + url: https://opensource.org/licenses/MIT version: 1.0.0 +servers: + - url: https://{region}.example.com/{version} + description: Regional server + variables: + region: + enum: + - eu + - us + default: eu + description: Data region + version: + default: v1 +security: + - api_key: [] + - petstore_auth: + - read:pets + - write:pets +tags: + - name: pets + description: Everything about pets + externalDocs: + description: Pet documentation + url: https://example.com/docs/pets +externalDocs: + description: Full documentation + url: https://example.com/docs +x-vendor-flag: true paths: /pets: get: From 88a1ac00d1fd0738e3ae92af0c9c651aafd04fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 07:51:19 +0200 Subject: [PATCH 05/20] test: cover every operation, parameter and response field of 3.0 --- tests/Cases/Schema/examples/complete-3-0.yaml | 179 +++++++++++++++++- 1 file changed, 177 insertions(+), 2 deletions(-) diff --git a/tests/Cases/Schema/examples/complete-3-0.yaml b/tests/Cases/Schema/examples/complete-3-0.yaml index 7d33d01..65c1666 100644 --- a/tests/Cases/Schema/examples/complete-3-0.yaml +++ b/tests/Cases/Schema/examples/complete-3-0.yaml @@ -39,8 +39,183 @@ externalDocs: url: https://example.com/docs x-vendor-flag: true paths: - /pets: + /pets/{petId}: + summary: A single pet + description: Operations on one pet + servers: + - url: https://pets.example.com + parameters: + - name: petId + in: path + description: Pet identifier + required: true + deprecated: false + style: simple + explode: false + schema: + type: string + example: abc123 get: + tags: + - pets + summary: Read a pet + description: Returns a single pet + externalDocs: + description: Operation documentation + url: https://example.com/docs/read-pet + operationId: readPet + deprecated: true + parameters: + - name: verbose + in: query + description: Return the long form + required: false + allowEmptyValue: true + allowReserved: true + style: form + explode: true + schema: + type: boolean + examples: + enabled: + summary: Verbose enabled + description: Ask for the long form + value: true + - name: X-Trace + in: header + required: false + style: simple + schema: + type: string + - name: session + in: cookie + required: false + style: form + content: + application/json: + schema: + type: object responses: '200': - description: A list of pets + description: The pet + headers: + X-Rate-Limit: + description: Calls left this hour + required: false + deprecated: false + style: simple + explode: false + schema: + type: integer + content: + application/json: + schema: + type: object + example: + id: abc123 + examples: + first: + summary: The first pet + value: + id: abc123 + links: + owner: + operationId: readOwner + parameters: + ownerId: $response.body#/ownerId + requestBody: $response.body + description: The owner of this pet + server: + url: https://owners.example.com + description: Owner service + default: + description: Unexpected error + callbacks: + petStatus: + '{$request.body#/callbackUrl}': + post: + requestBody: + description: Status update + required: true + content: + application/json: + schema: + type: object + responses: + '200': + description: Acknowledged + security: + - api_key: [] + servers: + - url: https://pets.example.com/v1 + put: + operationId: replacePet + requestBody: + description: The replacement pet + required: true + content: + application/json: + schema: + type: object + responses: + '200': + description: The replaced pet + post: + operationId: uploadPetPhoto + requestBody: + description: A multipart upload + required: true + content: + multipart/form-data: + schema: + type: object + properties: + profile: + type: object + avatar: + type: string + format: binary + encoding: + avatar: + contentType: image/png + headers: + X-Checksum: + description: Upload checksum + schema: + type: string + style: form + explode: false + allowReserved: false + responses: + '201': + description: Photo stored + delete: + operationId: deletePet + responses: + '204': + description: Deleted + options: + operationId: describePet + responses: + '200': + description: Allowed methods + head: + operationId: checkPet + responses: + '200': + description: Pet exists + patch: + operationId: updatePet + requestBody: + content: + application/merge-patch+json: + schema: + type: object + responses: + '200': + description: The updated pet + trace: + operationId: tracePet + responses: + '200': + description: Trace result From a424ec4489c1b736e72a98078e46525535bfdeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 07:58:30 +0200 Subject: [PATCH 06/20] test: cover every header field of 3.0 --- tests/Cases/Schema/examples/complete-3-0.yaml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Cases/Schema/examples/complete-3-0.yaml b/tests/Cases/Schema/examples/complete-3-0.yaml index 65c1666..7d0f544 100644 --- a/tests/Cases/Schema/examples/complete-3-0.yaml +++ b/tests/Cases/Schema/examples/complete-3-0.yaml @@ -103,10 +103,30 @@ paths: description: Calls left this hour required: false deprecated: false + allowEmptyValue: false + allowReserved: false style: simple explode: false schema: type: integer + example: 42 + X-Trace-Id: + description: Trace identifier + required: false + style: simple + schema: + type: string + examples: + main: + summary: Main trace + value: trace-12345 + X-Session-Info: + description: Session information + required: false + content: + application/json: + schema: + type: object content: application/json: schema: From be7c1132120db542cd2c364c6a5803edeb9ddc8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 08:02:48 +0200 Subject: [PATCH 07/20] test: cover every components section of 3.0 --- tests/Cases/Schema/examples/complete-3-0.yaml | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/tests/Cases/Schema/examples/complete-3-0.yaml b/tests/Cases/Schema/examples/complete-3-0.yaml index 7d0f544..885cb61 100644 --- a/tests/Cases/Schema/examples/complete-3-0.yaml +++ b/tests/Cases/Schema/examples/complete-3-0.yaml @@ -239,3 +239,102 @@ paths: responses: '200': description: Trace result +components: + schemas: + Pet: + type: object + required: + - id + properties: + id: + type: string + tag: + type: string + responses: + NotFound: + description: The pet does not exist + content: + application/json: + schema: + type: object + parameters: + PetIdParameter: + name: petId + in: path + required: true + schema: + type: string + examples: + PetExample: + summary: An example pet + description: A pet with the minimum fields + value: + id: abc123 + requestBodies: + PetBody: + description: A pet to store + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + headers: + XRateLimit: + description: Calls left this hour + schema: + type: integer + securitySchemes: + api_key: + type: apiKey + description: Key based authentication + name: api_key + in: header + basic_auth: + type: http + description: Basic authentication + scheme: basic + bearer_auth: + type: http + scheme: bearer + bearerFormat: JWT + petstore_auth: + type: oauth2 + description: OAuth 2 authentication + flows: + implicit: + authorizationUrl: https://example.com/oauth/authorize + refreshUrl: https://example.com/oauth/refresh + scopes: + read:pets: Read pets + password: + tokenUrl: https://example.com/oauth/token + refreshUrl: https://example.com/oauth/refresh + scopes: + read:pets: Read pets + clientCredentials: + tokenUrl: https://example.com/oauth/token + scopes: + write:pets: Write pets + authorizationCode: + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + refreshUrl: https://example.com/oauth/refresh + scopes: + read:pets: Read pets + write:pets: Write pets + openid_auth: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration + links: + OwnerLink: + operationRef: '#/paths/~1owners~1{ownerId}/get' + parameters: + ownerId: $response.body#/ownerId + description: The owner of this pet + callbacks: + PetStatusCallback: + '{$request.body#/callbackUrl}': + post: + responses: + '200': + description: Acknowledged From e6e4adcb459fb489080c0765ecf81fbe00494704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 08:11:39 +0200 Subject: [PATCH 08/20] test: make the complete 3.1 document a superset of the 3.0 one --- tests/Cases/Schema/examples/complete-3-1.yaml | 330 +++++++++++++++++- 1 file changed, 316 insertions(+), 14 deletions(-) diff --git a/tests/Cases/Schema/examples/complete-3-1.yaml b/tests/Cases/Schema/examples/complete-3-1.yaml index ab5173d..3748930 100644 --- a/tests/Cases/Schema/examples/complete-3-1.yaml +++ b/tests/Cases/Schema/examples/complete-3-1.yaml @@ -2,41 +2,246 @@ openapi: "3.1.1" jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema info: title: Complete 3.1 document - summary: Uses every field OpenAPI 3.1 defines. - version: 1.0.0 + summary: A complete 3.1 document. + description: Uses every field OpenAPI 3.1 defines. + termsOfService: https://example.com/terms + contact: + name: API Support + url: https://example.com/support + email: support@example.com license: name: MIT identifier: MIT + version: 1.0.0 +servers: + - url: https://{region}.example.com/{version} + description: Regional server + variables: + region: + enum: + - eu + - us + default: eu + description: Data region + version: + default: v1 +security: + - api_key: [] + - petstore_auth: + - read:pets + - write:pets +tags: + - name: pets + description: Everything about pets + externalDocs: + description: Pet documentation + url: https://example.com/docs/pets +externalDocs: + description: Full documentation + url: https://example.com/docs +x-vendor-flag: true +webhooks: + petCreated: + post: + requestBody: + content: + application/json: + schema: + type: object + const: null + responses: + '200': + description: Acknowledged + petDeleted: + $ref: '#/components/pathItems/petEvent' + summary: Reference summary + description: Reference description paths: - /pets: + /pets/{petId}: + summary: A single pet + description: Operations on one pet + servers: + - url: https://pets.example.com + parameters: + - name: petId + in: path + description: Pet identifier + required: true + deprecated: false + style: simple + explode: false + schema: + type: string + example: abc123 get: + tags: + - pets + summary: Read a pet + description: Returns a single pet + externalDocs: + description: Operation documentation + url: https://example.com/docs/read-pet + operationId: readPet + deprecated: true + parameters: + - name: verbose + in: query + description: Return the long form + required: false + allowEmptyValue: true + allowReserved: true + style: form + explode: true + schema: + type: boolean + examples: + enabled: + summary: Verbose enabled + description: Ask for the long form + value: true + - name: X-Trace + in: header + required: false + style: simple + schema: + type: string + - name: session + in: cookie + required: false + style: form + content: + application/json: + schema: + type: object responses: '200': - description: A list of pets + description: The pet + headers: + X-Rate-Limit: + description: Calls left this hour + required: false + deprecated: false + style: simple + explode: false + schema: + type: integer content: application/json: schema: - type: [array, "null"] + type: [object, "null"] + const: null prefixItems: - type: string unevaluatedProperties: false contentMediaType: application/json -webhooks: - petCreated: - post: + example: + id: abc123 + examples: + first: + summary: The first pet + value: + id: abc123 + links: + owner: + operationId: readOwner + parameters: + ownerId: $response.body#/ownerId + requestBody: $response.body + description: The owner of this pet + server: + url: https://owners.example.com + description: Owner service + default: + description: Unexpected error + callbacks: + petStatus: + '{$request.body#/callbackUrl}': + post: + requestBody: + description: Status update + required: true + content: + application/json: + schema: + type: object + responses: + '200': + description: Acknowledged + security: + - api_key: [] + servers: + - url: https://pets.example.com/v1 + put: + operationId: replacePet requestBody: + description: The replacement pet + required: true content: application/json: schema: type: object - const: null responses: '200': - description: Acknowledged - petDeleted: - $ref: '#/components/pathItems/petEvent' - summary: Reference summary - description: Reference description + description: The replaced pet + post: + operationId: uploadPetPhoto + requestBody: + description: A multipart upload + required: true + content: + multipart/form-data: + schema: + type: object + properties: + profile: + type: object + avatar: + type: string + format: binary + encoding: + avatar: + contentType: image/png + headers: + X-Checksum: + description: Upload checksum + schema: + type: string + style: form + explode: false + allowReserved: false + responses: + '201': + description: Photo stored + delete: + operationId: deletePet + responses: + '204': + description: Deleted + options: + operationId: describePet + responses: + '200': + description: Allowed methods + head: + operationId: checkPet + responses: + '200': + description: Pet exists + patch: + operationId: updatePet + requestBody: + content: + application/merge-patch+json: + schema: + type: object + responses: + '200': + description: The updated pet + trace: + operationId: tracePet + responses: + '200': + description: Trace result components: pathItems: petEvent: @@ -44,7 +249,104 @@ components: responses: '200': description: Acknowledged + schemas: + Pet: + type: object + required: + - id + properties: + id: + type: string + tag: + type: string + responses: + NotFound: + description: The pet does not exist + content: + application/json: + schema: + type: object + parameters: + PetIdParameter: + name: petId + in: path + required: true + schema: + type: string + examples: + PetExample: + summary: An example pet + description: A pet with the minimum fields + value: + id: abc123 + requestBodies: + PetBody: + description: A pet to store + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + headers: + XRateLimit: + description: Calls left this hour + schema: + type: integer securitySchemes: + api_key: + type: apiKey + description: Key based authentication + name: api_key + in: header + basic_auth: + type: http + description: Basic authentication + scheme: basic + bearer_auth: + type: http + scheme: bearer + bearerFormat: JWT mtls: type: mutualTLS description: Client certificate authentication + petstore_auth: + type: oauth2 + description: OAuth 2 authentication + flows: + implicit: + authorizationUrl: https://example.com/oauth/authorize + refreshUrl: https://example.com/oauth/refresh + scopes: + read:pets: Read pets + password: + tokenUrl: https://example.com/oauth/token + refreshUrl: https://example.com/oauth/refresh + scopes: + read:pets: Read pets + clientCredentials: + tokenUrl: https://example.com/oauth/token + scopes: + write:pets: Write pets + authorizationCode: + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + refreshUrl: https://example.com/oauth/refresh + scopes: + read:pets: Read pets + write:pets: Write pets + openid_auth: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration + links: + OwnerLink: + operationRef: '#/paths/~1owners~1{ownerId}/get' + parameters: + ownerId: $response.body#/ownerId + description: The owner of this pet + callbacks: + PetStatusCallback: + '{$request.body#/callbackUrl}': + post: + responses: + '200': + description: Acknowledged From b113a0f895142dd448bef347810f3700b5324bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 08:17:53 +0200 Subject: [PATCH 09/20] test: keep the complete 3.1 document a superset of the 3.0 one --- tests/Cases/Schema/examples/complete-3-1.yaml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Cases/Schema/examples/complete-3-1.yaml b/tests/Cases/Schema/examples/complete-3-1.yaml index 3748930..f09fb97 100644 --- a/tests/Cases/Schema/examples/complete-3-1.yaml +++ b/tests/Cases/Schema/examples/complete-3-1.yaml @@ -121,10 +121,30 @@ paths: description: Calls left this hour required: false deprecated: false + allowEmptyValue: false + allowReserved: false style: simple explode: false schema: type: integer + example: 42 + X-Trace-Id: + description: Trace identifier + required: false + style: simple + schema: + type: string + examples: + main: + summary: Main trace + value: trace-12345 + X-Session-Info: + description: Session information + required: false + content: + application/json: + schema: + type: object content: application/json: schema: From 633e635abf7edecfae9589e77f53cf5bcb2dc2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 08:23:01 +0200 Subject: [PATCH 10/20] docs: state which OpenAPI versions are supported --- .docs/README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.docs/README.md b/.docs/README.md index 5a606fb..5bd1673 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -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) @@ -46,6 +47,18 @@ composer require contributte/openapi - [ServerVariable.php](../src/Schema/ServerVariable.php) - [Tag.php](../src/Schema/Tag.php) +## Supported versions + +| OpenAPI | Support | +|---------|---------| +| 3.0 | full | +| 3.1 | full | + +Both claims are backed by a test, not by hand: `tests/Cases/VersionSupportTest.php` round-trips +a complete document per version - one that uses every field the version defines - and requires +`VersionValidator` to report nothing against it. See `tests/Cases/Schema/examples/complete-3-0.yaml` +and `complete-3-1.yaml`. + ## Version validation The schema classes accept any document, whatever version it declares. `VersionValidator` From 6c5762968ca8bc6359ed08d0b36918de2246f6e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 08:45:01 +0200 Subject: [PATCH 11/20] test: declare widely supported patch versions in the complete documents --- tests/Cases/Schema/examples/complete-3-0.yaml | 2 +- tests/Cases/Schema/examples/complete-3-1.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Cases/Schema/examples/complete-3-0.yaml b/tests/Cases/Schema/examples/complete-3-0.yaml index 885cb61..c3bb08f 100644 --- a/tests/Cases/Schema/examples/complete-3-0.yaml +++ b/tests/Cases/Schema/examples/complete-3-0.yaml @@ -1,4 +1,4 @@ -openapi: "3.0.4" +openapi: "3.0.3" info: title: Complete 3.0 document description: Uses every field OpenAPI 3.0 defines. diff --git a/tests/Cases/Schema/examples/complete-3-1.yaml b/tests/Cases/Schema/examples/complete-3-1.yaml index f09fb97..3496b6b 100644 --- a/tests/Cases/Schema/examples/complete-3-1.yaml +++ b/tests/Cases/Schema/examples/complete-3-1.yaml @@ -1,4 +1,4 @@ -openapi: "3.1.1" +openapi: "3.1.0" jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema info: title: Complete 3.1 document From c7c560a25a0350f225c722c620ba952d7caea3f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 08:45:28 +0200 Subject: [PATCH 12/20] test: use the OAS dialect as the jsonSchemaDialect value --- tests/Cases/Schema/examples/complete-3-1.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Cases/Schema/examples/complete-3-1.yaml b/tests/Cases/Schema/examples/complete-3-1.yaml index 3496b6b..a2680d0 100644 --- a/tests/Cases/Schema/examples/complete-3-1.yaml +++ b/tests/Cases/Schema/examples/complete-3-1.yaml @@ -1,5 +1,5 @@ openapi: "3.1.0" -jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base info: title: Complete 3.1 document summary: A complete 3.1 document. From e8ecad9aff8a35ce13de671c366cd2ac729c5ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 08:46:05 +0200 Subject: [PATCH 13/20] test: drop query-only traits from the header objects --- tests/Cases/Schema/examples/complete-3-0.yaml | 2 -- tests/Cases/Schema/examples/complete-3-1.yaml | 2 -- 2 files changed, 4 deletions(-) diff --git a/tests/Cases/Schema/examples/complete-3-0.yaml b/tests/Cases/Schema/examples/complete-3-0.yaml index c3bb08f..c0db5bb 100644 --- a/tests/Cases/Schema/examples/complete-3-0.yaml +++ b/tests/Cases/Schema/examples/complete-3-0.yaml @@ -103,8 +103,6 @@ paths: description: Calls left this hour required: false deprecated: false - allowEmptyValue: false - allowReserved: false style: simple explode: false schema: diff --git a/tests/Cases/Schema/examples/complete-3-1.yaml b/tests/Cases/Schema/examples/complete-3-1.yaml index a2680d0..d7048f1 100644 --- a/tests/Cases/Schema/examples/complete-3-1.yaml +++ b/tests/Cases/Schema/examples/complete-3-1.yaml @@ -121,8 +121,6 @@ paths: description: Calls left this hour required: false deprecated: false - allowEmptyValue: false - allowReserved: false style: simple explode: false schema: From 1da92cd4aef4b970fa27f094af995ff058b5b590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 08:46:58 +0200 Subject: [PATCH 14/20] test: point the links at operations the documents define --- tests/Cases/Schema/examples/complete-3-0.yaml | 4 ++-- tests/Cases/Schema/examples/complete-3-1.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Cases/Schema/examples/complete-3-0.yaml b/tests/Cases/Schema/examples/complete-3-0.yaml index c0db5bb..b248a09 100644 --- a/tests/Cases/Schema/examples/complete-3-0.yaml +++ b/tests/Cases/Schema/examples/complete-3-0.yaml @@ -138,7 +138,7 @@ paths: id: abc123 links: owner: - operationId: readOwner + operationId: readPet parameters: ownerId: $response.body#/ownerId requestBody: $response.body @@ -325,7 +325,7 @@ components: openIdConnectUrl: https://example.com/.well-known/openid-configuration links: OwnerLink: - operationRef: '#/paths/~1owners~1{ownerId}/get' + operationRef: '#/paths/~1pets~1{petId}/get' parameters: ownerId: $response.body#/ownerId description: The owner of this pet diff --git a/tests/Cases/Schema/examples/complete-3-1.yaml b/tests/Cases/Schema/examples/complete-3-1.yaml index d7048f1..21bc765 100644 --- a/tests/Cases/Schema/examples/complete-3-1.yaml +++ b/tests/Cases/Schema/examples/complete-3-1.yaml @@ -161,7 +161,7 @@ paths: id: abc123 links: owner: - operationId: readOwner + operationId: readPet parameters: ownerId: $response.body#/ownerId requestBody: $response.body @@ -357,7 +357,7 @@ components: openIdConnectUrl: https://example.com/.well-known/openid-configuration links: OwnerLink: - operationRef: '#/paths/~1owners~1{ownerId}/get' + operationRef: '#/paths/~1pets~1{petId}/get' parameters: ownerId: $response.body#/ownerId description: The owner of this pet From 3f44dcd08f1685a1cc5acffe57538504790bf7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 08:47:44 +0200 Subject: [PATCH 15/20] test: make the 2020-12 keyword example coherent --- tests/Cases/Schema/examples/complete-3-1.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/Cases/Schema/examples/complete-3-1.yaml b/tests/Cases/Schema/examples/complete-3-1.yaml index 21bc765..4611f3e 100644 --- a/tests/Cases/Schema/examples/complete-3-1.yaml +++ b/tests/Cases/Schema/examples/complete-3-1.yaml @@ -147,9 +147,11 @@ paths: application/json: schema: type: [object, "null"] - const: null - prefixItems: - - type: string + properties: + history: + type: array + prefixItems: + - type: string unevaluatedProperties: false contentMediaType: application/json example: From d13c21a5d1297f9b29affc22400f5a4de9273709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 08:48:54 +0200 Subject: [PATCH 16/20] refactor: keep the optional-since table private --- src/Validator/VersionValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Validator/VersionValidator.php b/src/Validator/VersionValidator.php index 50d671e..a877cd6 100644 --- a/src/Validator/VersionValidator.php +++ b/src/Validator/VersionValidator.php @@ -43,7 +43,7 @@ class VersionValidator /** * Required fields and the version that made them optional. */ - public const FIELD_OPTIONAL_SINCE = [ + private const FIELD_OPTIONAL_SINCE = [ 'paths' => Version::V3_1, ]; From f864b4f7e4d2bb02e1612c0aa1f5203001242255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 08:49:56 +0200 Subject: [PATCH 17/20] test: stop the coverage table fold from hiding a version --- tests/Cases/VersionSupportTest.php | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/Cases/VersionSupportTest.php b/tests/Cases/VersionSupportTest.php index a662817..87099ab 100644 --- a/tests/Cases/VersionSupportTest.php +++ b/tests/Cases/VersionSupportTest.php @@ -134,27 +134,37 @@ private static function recursiveSort(array $data): array * oldest supported version, up to and including this one. The validator's tables are the * list of what the library knows about versions, so they double as the coverage list. * + * Both tables contribute "(path, introducedIn)" pairs to a flat list rather than a + * path-keyed map, so a path that carries several values introduced in different versions + * cannot have one overwrite another before the version filter runs. + * * @return string[] */ private static function expectedPaths(string $version): array { $oldest = Version::SUPPORTED[0]; - $paths = []; - $introducedIn = VersionValidator::FIELD_INTRODUCED_IN; + $pairs = []; + + foreach (VersionValidator::FIELD_INTRODUCED_IN as $path => $introduced) { + $pairs[] = [$path, $introduced]; + } foreach (VersionValidator::VALUE_INTRODUCED_IN as $path => $values) { foreach ($values as $introduced) { - $introducedIn[$path] = $introduced; + $pairs[] = [$path, $introduced]; } } - foreach ($introducedIn as $path => $introduced) { + $paths = []; + + foreach ($pairs as [$path, $introduced]) { if (Version::isBefore($oldest, $introduced) && !Version::isBefore($version, $introduced)) { - $paths[] = $path; + $paths[$path] = true; } } + $paths = array_keys($paths); sort($paths); return $paths; From 7b6892e408212729bf758e2fb4cf61a7d7775159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 09:01:09 +0200 Subject: [PATCH 18/20] fix: keep specification extensions in headers and security schemes Header, SecurityScheme and OAuthFlow silently dropped x- specification extensions on round-trip, unlike every other schema class. Wire in VendorExtensions the same way Contact and Server already do. --- src/Schema/Header.php | 18 ++++++++++++++++++ src/Schema/OAuthFlow.php | 21 ++++++++++++++++++++- src/Schema/SecurityScheme.php | 17 +++++++++++++++++ tests/Cases/Schema/HeaderTest.php | 13 +++++++++++++ tests/Cases/Schema/OAuthFlowTest.php | 14 ++++++++++++++ tests/Cases/Schema/SecuritySchemeTest.php | 15 +++++++++++++++ 6 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/Schema/Header.php b/src/Schema/Header.php index 7e5885e..feda321 100644 --- a/src/Schema/Header.php +++ b/src/Schema/Header.php @@ -29,6 +29,8 @@ class Header /** @var MediaType[]|null */ private ?array $content = null; + private ?VendorExtensions $vendorExtensions = null; + /** * @param mixed[] $data */ @@ -62,6 +64,8 @@ public static function fromArray(array $data): Header $header->setContent($key, MediaType::fromArray($contentData)); } + $header->setVendorExtensions(VendorExtensions::fromArray($data)); + return $header; } @@ -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; } @@ -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; + } + } diff --git a/src/Schema/OAuthFlow.php b/src/Schema/OAuthFlow.php index ef3321c..1ecf0d2 100644 --- a/src/Schema/OAuthFlow.php +++ b/src/Schema/OAuthFlow.php @@ -14,6 +14,8 @@ class OAuthFlow /** @var array */ private array $scopes; + private ?VendorExtensions $vendorExtensions = null; + /** * @param array $scopes */ @@ -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; } /** @@ -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; } @@ -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; + } + } diff --git a/src/Schema/SecurityScheme.php b/src/Schema/SecurityScheme.php index b06a3b5..9a3f8c4 100644 --- a/src/Schema/SecurityScheme.php +++ b/src/Schema/SecurityScheme.php @@ -60,6 +60,8 @@ class SecurityScheme private ?string $openIdConnectUrl = null; + private ?VendorExtensions $vendorExtensions = null; + public function __construct(string $type) { $this->setType($type); @@ -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; } @@ -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; } @@ -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, [ diff --git a/tests/Cases/Schema/HeaderTest.php b/tests/Cases/Schema/HeaderTest.php index d322e9f..398c5d1 100644 --- a/tests/Cases/Schema/HeaderTest.php +++ b/tests/Cases/Schema/HeaderTest.php @@ -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(); diff --git a/tests/Cases/Schema/OAuthFlowTest.php b/tests/Cases/Schema/OAuthFlowTest.php index de83157..76d15e4 100644 --- a/tests/Cases/Schema/OAuthFlowTest.php +++ b/tests/Cases/Schema/OAuthFlowTest.php @@ -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 diff --git a/tests/Cases/Schema/SecuritySchemeTest.php b/tests/Cases/Schema/SecuritySchemeTest.php index f8b5919..554bcc0 100644 --- a/tests/Cases/Schema/SecuritySchemeTest.php +++ b/tests/Cases/Schema/SecuritySchemeTest.php @@ -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 { From 613dee39cac0516bae1af247b1ad3f143298f0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 09:03:12 +0200 Subject: [PATCH 19/20] test: exercise extensions on headers, schemes and flows Add an x- key to a Header, a Security Scheme and an OAuth Flow Object in both complete documents so VersionSupportTest's round-trip assertion proves the extension fix end to end, not just the isolated unit tests. --- tests/Cases/Schema/examples/complete-3-0.yaml | 3 +++ tests/Cases/Schema/examples/complete-3-1.yaml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tests/Cases/Schema/examples/complete-3-0.yaml b/tests/Cases/Schema/examples/complete-3-0.yaml index b248a09..97cfb48 100644 --- a/tests/Cases/Schema/examples/complete-3-0.yaml +++ b/tests/Cases/Schema/examples/complete-3-0.yaml @@ -108,6 +108,7 @@ paths: schema: type: integer example: 42 + x-internal: true X-Trace-Id: description: Trace identifier required: false @@ -287,6 +288,7 @@ components: description: Key based authentication name: api_key in: header + x-internal: true basic_auth: type: http description: Basic authentication @@ -304,6 +306,7 @@ components: refreshUrl: https://example.com/oauth/refresh scopes: read:pets: Read pets + x-internal: true password: tokenUrl: https://example.com/oauth/token refreshUrl: https://example.com/oauth/refresh diff --git a/tests/Cases/Schema/examples/complete-3-1.yaml b/tests/Cases/Schema/examples/complete-3-1.yaml index 4611f3e..5714e47 100644 --- a/tests/Cases/Schema/examples/complete-3-1.yaml +++ b/tests/Cases/Schema/examples/complete-3-1.yaml @@ -126,6 +126,7 @@ paths: schema: type: integer example: 42 + x-internal: true X-Trace-Id: description: Trace identifier required: false @@ -318,6 +319,7 @@ components: description: Key based authentication name: api_key in: header + x-internal: true basic_auth: type: http description: Basic authentication @@ -338,6 +340,7 @@ components: refreshUrl: https://example.com/oauth/refresh scopes: read:pets: Read pets + x-internal: true password: tokenUrl: https://example.com/oauth/token refreshUrl: https://example.com/oauth/refresh From 688680b094144ed30fb9a226b9cbaa441cf76c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Fi=C5=A1er?= Date: Mon, 3 Aug 2026 09:08:33 +0200 Subject: [PATCH 20/20] docs: state only what the version support test enforces --- .docs/README.md | 17 ++++++++++------- tests/Cases/Schema/examples/complete-3-0.yaml | 2 +- tests/Cases/Schema/examples/complete-3-1.yaml | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.docs/README.md b/.docs/README.md index 5bd1673..cdd97dc 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -51,13 +51,16 @@ composer require contributte/openapi | OpenAPI | Support | |---------|---------| -| 3.0 | full | -| 3.1 | full | - -Both claims are backed by a test, not by hand: `tests/Cases/VersionSupportTest.php` round-trips -a complete document per version - one that uses every field the version defines - and requires -`VersionValidator` to report nothing against it. See `tests/Cases/Schema/examples/complete-3-0.yaml` -and `complete-3-1.yaml`. +| 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 diff --git a/tests/Cases/Schema/examples/complete-3-0.yaml b/tests/Cases/Schema/examples/complete-3-0.yaml index 97cfb48..9438391 100644 --- a/tests/Cases/Schema/examples/complete-3-0.yaml +++ b/tests/Cases/Schema/examples/complete-3-0.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: title: Complete 3.0 document - description: Uses every field OpenAPI 3.0 defines. + description: Uses every field OpenAPI 3.0 defines, except where two fields exclude each other. termsOfService: https://example.com/terms contact: name: API Support diff --git a/tests/Cases/Schema/examples/complete-3-1.yaml b/tests/Cases/Schema/examples/complete-3-1.yaml index 5714e47..21b9912 100644 --- a/tests/Cases/Schema/examples/complete-3-1.yaml +++ b/tests/Cases/Schema/examples/complete-3-1.yaml @@ -3,7 +3,7 @@ jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base info: title: Complete 3.1 document summary: A complete 3.1 document. - description: Uses every field OpenAPI 3.1 defines. + description: Uses every field OpenAPI 3.1 defines, except where two fields exclude each other. termsOfService: https://example.com/terms contact: name: API Support