Add validation rule that example/examples are mutually exclusive and add examples property to Schema (3.1.0 only) (#1561)

This commit is contained in:
Martin Rademacher 2024-03-23 19:35:46 +13:00 committed by GitHub
parent d3265da27c
commit 2357fafbb0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
87 changed files with 1084 additions and 166 deletions

View File

@ -84,7 +84,9 @@
"testlegacy": "Run tests using the legacy TokenAnalyser",
"testall": "Run all tests (test + testlegacy)",
"analyse": "Run static analysis (phpstan/psalm)",
"spectral": "Run spectral lint over all .yaml files in the Examples folder",
"spectral-examples": "Run spectral lint over all .yaml files in the Examples folder",
"spectral-scratch": "Run spectral lint over all .yaml files in the tests/Fixtures/Scratch folder",
"spectral": "Run all spectral tests",
"docs:gen": "Rebuild reference documentation",
"docs:dev": "Run dev server for local development of gh-pages",
"docs:build": "Re-build static gh-pages"
@ -105,7 +107,12 @@
"export XDEBUG_MODE=off && phpstan analyse --memory-limit=2G",
"export XDEBUG_MODE=off && psalm"
],
"spectral": "for ff in `find Examples -name '*.yaml'`; do spectral lint $ff; done",
"spectral-examples": "for ff in `find Examples -name '*.yaml'`; do spectral lint $ff; done",
"spectral-scratch": "for ff in `find tests/Fixtures/Scratch -name '*.yaml'`; do spectral lint $ff; done",
"spectral": [
"@spectral-examples",
"@spectral-scratch"
],
"docs:gen": [
"@php tools/refgen.php",
"@php tools/procgen.php"

View File

@ -86,12 +86,12 @@ parameters:
path: src/Processors/DocBlockDescriptions.php
-
message: "#^Property OpenApi\\\\Annotations\\\\JsonContent\\:\\:\\$examples \\(array\\<string, OpenApi\\\\Annotations\\\\Examples\\>\\) does not accept string\\.$#"
message: "#^Property OpenApi\\\\Annotations\\\\Schema\\:\\:\\$examples \\(array\\<OpenApi\\\\Annotations\\\\Examples\\>\\) does not accept string\\.$#"
count: 1
path: src/Processors/MergeJsonContent.php
-
message: "#^Property OpenApi\\\\Annotations\\\\XmlContent\\:\\:\\$examples \\(array\\<string, OpenApi\\\\Annotations\\\\Examples\\>\\) does not accept string\\.$#"
message: "#^Property OpenApi\\\\Annotations\\\\Schema\\:\\:\\$examples \\(array\\<OpenApi\\\\Annotations\\\\Examples\\>\\) does not accept string\\.$#"
count: 1
path: src/Processors/MergeXmlContent.php

View File

@ -165,6 +165,16 @@ abstract class AbstractAnnotation implements \JsonSerializable
$this->_context->logger->warning('Ignoring unexpected property "' . $property . '" for ' . $this->identity() . ', expecting "' . implode('", "', array_keys($fields)) . '" in ' . $this->_context);
}
/**
* Check if one of the given version numbers matches the current OpenAPI version.
*
* @param string|array $versions One or more version numbers
*/
public function isOpenApiVersion($versions): bool
{
return $this->_context->isVersion($versions);
}
/**
* Merge given annotations to their mapped properties configured in static::$_nested.
*
@ -350,7 +360,7 @@ abstract class AbstractAnnotation implements \JsonSerializable
if (isset($data->ref)) {
// Only specific https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#reference-object
$ref = ['$ref' => $data->ref];
if ($this->_context->version === OpenApi::VERSION_3_1_0) {
if ($this->isOpenApiVersion(OpenApi::VERSION_3_1_0)) {
foreach (['summary', 'description'] as $prop) {
if (property_exists($this, $prop)) {
if (!Generator::isDefault($this->{$prop})) {
@ -361,7 +371,7 @@ abstract class AbstractAnnotation implements \JsonSerializable
}
if (property_exists($this, 'nullable') && $this->nullable === true) {
$ref = ['oneOf' => [$ref]];
if ($this->_context->version == OpenApi::VERSION_3_1_0) {
if ($this->isOpenApiVersion(OpenApi::VERSION_3_1_0)) {
$ref['oneOf'][] = ['type' => 'null'];
} else {
$ref['nullable'] = $data->nullable;
@ -381,7 +391,18 @@ abstract class AbstractAnnotation implements \JsonSerializable
$data = (object) $ref;
}
if ($this->_context->version === OpenApi::VERSION_3_1_0) {
if ($this->isOpenApiVersion(OpenApi::VERSION_3_0_0)) {
if (isset($data->exclusiveMinimum) && is_numeric($data->exclusiveMinimum)) {
$data->minimum = $data->exclusiveMinimum;
$data->exclusiveMinimum = true;
}
if (isset($data->exclusiveMaximum) && is_numeric($data->exclusiveMaximum)) {
$data->maximum = $data->exclusiveMaximum;
$data->exclusiveMaximum = true;
}
}
if ($this->isOpenApiVersion(OpenApi::VERSION_3_1_0)) {
if (isset($data->nullable)) {
if (true === $data->nullable) {
if (isset($data->oneOf)) {
@ -540,6 +561,13 @@ abstract class AbstractAnnotation implements \JsonSerializable
}
$stack[] = $this;
if (property_exists($this, 'example') && property_exists($this, 'examples')) {
if (!Generator::isDefault($this->example) && !Generator::isDefault($this->examples)) {
$valid = false;
$this->_context->logger->warning($this->identity() . ': "example" and "examples" are mutually exclusive');
}
}
return self::_validate($this, $stack, $skip, $ref, $context) ? $valid : false;
}

View File

@ -54,7 +54,7 @@ class Components extends AbstractAnnotation
/**
* Reusable Examples.
*
* @var Examples[]
* @var array<Examples>
*/
public $examples = Generator::UNDEFINED;

View File

@ -82,6 +82,7 @@ class Examples extends AbstractAnnotation
public static $_parents = [
Components::class,
Schema::class,
Parameter::class,
PathParameter::class,
MediaType::class,

View File

@ -6,6 +6,8 @@
namespace OpenApi\Annotations;
use OpenApi\Annotations as OA;
/**
* A `@OA\Request` header parameter.
*

View File

@ -6,7 +6,7 @@
namespace OpenApi\Annotations;
use OpenApi\Generator;
use OpenApi\Annotations as OA;
/**
* Shorthand for a json response.
@ -17,16 +17,6 @@ use OpenApi\Generator;
*/
class JsonContent extends Schema
{
/**
* An associative array of Examples attributes.
*
* The keys represent the name of the example and the values are instances of the Examples attribute.
* Each example is used to show how the content of the request or response should look like.
*
* @var array<string,Examples>
*/
public $examples = Generator::UNDEFINED;
/**
* @inheritdoc
*/

View File

@ -76,7 +76,7 @@ class License extends AbstractAnnotation
{
$data = parent::jsonSerialize();
if ($this->_context->isVersion(OpenApi::VERSION_3_0_0)) {
if ($this->isOpenApiVersion(OpenApi::VERSION_3_0_0)) {
unset($data->identifier);
}
@ -90,7 +90,7 @@ class License extends AbstractAnnotation
{
$valid = parent::validate($stack, $skip, $ref, $context);
if ($this->_context->isVersion(OpenApi::VERSION_3_1_0)) {
if ($this->isOpenApiVersion(OpenApi::VERSION_3_1_0)) {
if (!Generator::isDefault($this->url) && $this->identifier !== Generator::UNDEFINED) {
$this->_context->logger->warning($this->identity() . ' url and identifier are mutually exclusive');
$valid = false;

View File

@ -45,13 +45,11 @@ class MediaType extends AbstractAnnotation
/**
* Examples of the media type.
*
* Each example object should match the media type and specified schema if present.
* Each example should contain a value in the correct format as specified in the parameter encoding.
* The examples object is mutually exclusive of the example object.
* Furthermore, if referencing a schema which contains an example, the examples value shall override the example provided by the schema.
*
* Furthermore, if referencing a schema which contains an example,
* the examples value shall override the example provided by the schema.
*
* @var array<string,Examples>
* @var array<Examples>
*/
public $examples = Generator::UNDEFINED;

View File

@ -262,7 +262,7 @@ class OpenApi extends AbstractAnnotation
{
$data = parent::jsonSerialize();
if (false === $this->_context->isVersion(OpenApi::VERSION_3_1_0)) {
if (false === $this->isOpenApiVersion(OpenApi::VERSION_3_1_0)) {
unset($data->webhooks);
}

View File

@ -7,6 +7,7 @@
namespace OpenApi\Annotations;
use OpenApi\Generator;
use OpenApi\Annotations as OA;
/**
* Base class for `@OA\Get`, `@OA\Post`, `@OA\Put`, etc.

View File

@ -147,13 +147,13 @@ class Parameter extends AbstractAnnotation
public $example = Generator::UNDEFINED;
/**
* Examples of the media type.
* Examples of the parameter.
*
* Each example should contain a value in the correct format as specified in the parameter encoding.
* The examples object is mutually exclusive of the example object.
* Furthermore, if referencing a schema which contains an example, the examples value shall override the example provided by the schema.
*
* @var array<string,Examples>
* @var array<Examples>
*/
public $examples = Generator::UNDEFINED;

View File

@ -6,6 +6,8 @@
namespace OpenApi\Annotations;
use OpenApi\Annotations as OA;
/**
* A `@OA\Request` path parameter.
*

View File

@ -6,6 +6,8 @@
namespace OpenApi\Annotations;
use OpenApi\Annotations as OA;
/**
* A `@OA\Request` query parameter.
*

View File

@ -57,7 +57,8 @@ class Schema extends AbstractAnnotation
/**
* The maximum number of properties allowed in an object instance.
* An object instance is valid against this property if its number of properties is less than, or equal to, the value of this attribute.
* An object instance is valid against this property if its number of properties is less than, or equal to, the
* value of this attribute.
*
* @var int
*/
@ -65,7 +66,8 @@ class Schema extends AbstractAnnotation
/**
* The minimum number of properties allowed in an object instance.
* An object instance is valid against this property if its number of properties is greater than, or equal to, the value of this attribute.
* An object instance is valid against this property if its number of properties is greater than, or equal to, the
* value of this attribute.
*
* @var int
*/
@ -121,9 +123,9 @@ class Schema extends AbstractAnnotation
* - ssv: space separated values foo bar.
* - tsv: tab separated values foo\tbar.
* - pipes: pipe separated values foo|bar.
* - multi: corresponds to multiple parameter instances instead of multiple values for a single instance foo=bar&foo=baz.
* This is valid only for parameters of type <code>query</code> or <code>formData</code>.
* Default value is csv.
* - multi: corresponds to multiple parameter instances instead of multiple values for a single instance
* foo=bar&foo=baz. This is valid only for parameters of type <code>query</code> or <code>formData</code>. Default
* value is csv.
*
* @var string
*/
@ -179,7 +181,8 @@ class Schema extends AbstractAnnotation
/**
* The maximum length of a string property.
*
* A string instance is valid against this property if its length is less than, or equal to, the value of this attribute.
* A string instance is valid against this property if its length is less than, or equal to, the value of this
* attribute.
*
* @see [JSON schema validation](http://json-schema.org/latest/json-schema-validation.html#anchor26)
*
@ -190,7 +193,8 @@ class Schema extends AbstractAnnotation
/**
* The minimum length of a string property.
*
* A string instance is valid against this property if its length is greater than, or equal to, the value of this attribute.
* A string instance is valid against this property if its length is greater than, or equal to, the value of this
* attribute.
*
* @see [JSON schema validation](http://json-schema.org/latest/json-schema-validation.html#anchor29)
*
@ -208,7 +212,8 @@ class Schema extends AbstractAnnotation
/**
* The maximum number of items allowed in an array property.
*
* An array instance is valid against this property if its number of items is less than, or equal to, the value of this attribute.
* An array instance is valid against this property if its number of items is less than, or equal to, the value of
* this attribute.
*
* @see [JSON schema validation](http://json-schema.org/latest/json-schema-validation.html#anchor42)
*
@ -219,7 +224,8 @@ class Schema extends AbstractAnnotation
/**
* The minimum number of items allowed in an array property.
*
* An array instance is valid against this property if its number of items is greater than, or equal to, the value of this attribute.
* An array instance is valid against this property if its number of items is greater than, or equal to, the value
* of this attribute.
*
* @see [JSON schema validation](http://json-schema.org/latest/json-schema-validation.html#anchor45)
*
@ -241,7 +247,8 @@ class Schema extends AbstractAnnotation
/**
* A collection of allowable values for a property.
*
* A property instance is valid against this attribute if its value is one of the values specified in this collection.
* A property instance is valid against this attribute if its value is one of the values specified in this
* collection.
*
* @see [JSON schema validation](http://json-schema.org/latest/json-schema-validation.html#anchor76)
*
@ -319,6 +326,19 @@ class Schema extends AbstractAnnotation
*/
public $example = Generator::UNDEFINED;
/**
* Examples of the schema.
*
* Each example should contain a value in the correct format as specified in the parameter encoding.
* The examples object is mutually exclusive of the example object.
* Furthermore, if referencing a schema which contains an example, the examples value shall override the example provided by the schema.
*
* @since 3.1.0
*
* @var array<Examples>
*/
public $examples = Generator::UNDEFINED;
/**
* Allows sending a null value for the defined schema.
* Default value is false.
@ -439,6 +459,7 @@ class Schema extends AbstractAnnotation
Items::class => 'items',
Property::class => ['properties', 'property'],
ExternalDocumentation::class => 'externalDocs',
Examples::class => ['examples', 'example'],
Xml::class => 'xml',
AdditionalProperties::class => 'additionalProperties',
Attachable::class => ['attachables'],
@ -463,8 +484,9 @@ class Schema extends AbstractAnnotation
{
$data = parent::jsonSerialize();
if (isset($data->const)) {
if ($this->_context->isVersion(OpenApi::VERSION_3_0_0)) {
if ($this->isOpenApiVersion(OpenApi::VERSION_3_0_0)) {
unset($data->examples);
if (isset($data->const)) {
$data->enum = [$data->const];
unset($data->const);
}
@ -484,6 +506,14 @@ class Schema extends AbstractAnnotation
return false;
}
if ($this->isOpenApiVersion(OpenApi::VERSION_3_0_0)) {
if (!Generator::isDefault($this->examples)) {
$this->_context->logger->warning($this->identity() . ' is only allowed for ' . OpenApi::VERSION_3_1_0);
return false;
}
}
return parent::validate($stack, $skip, $ref, $context);
}
}

View File

@ -6,7 +6,7 @@
namespace OpenApi\Annotations;
use OpenApi\Generator;
use OpenApi\Annotations as OA;
/**
* Shorthand for a xml response.
@ -17,11 +17,6 @@ use OpenApi\Generator;
*/
class XmlContent extends Schema
{
/**
* @var array<string,Examples>
*/
public $examples = Generator::UNDEFINED;
/**
* @inheritdoc
*/

View File

@ -16,7 +16,7 @@ class Components extends \OpenApi\Annotations\Components
* @param Response[]|null $responses
* @param Parameter[]|null $parameters
* @param RequestBody[]|null $requestBodies
* @param Examples[]|null $examples
* @param array<Examples>|null $examples
* @param Header[]|null $headers
* @param SecurityScheme[]|null $securitySchemes
* @param Link[]|null $links

View File

@ -14,7 +14,7 @@ class JsonContent extends \OpenApi\Annotations\JsonContent
/**
* @param string|non-empty-array<string>|null $type
* @param string|class-string|object|null $ref
* @param array<string,Examples> $examples
* @param array<Examples> $examples
* @param string[] $required
* @param Property[] $properties
* @param int|float $maximum

View File

@ -12,7 +12,7 @@ use OpenApi\Generator;
class MediaType extends \OpenApi\Annotations\MediaType
{
/**
* @param array<string,Examples> $examples
* @param array<Examples> $examples
* @param array<string,mixed> $encoding
* @param array<string,mixed>|null $x
* @param Attachable[]|null $attachables

View File

@ -12,7 +12,7 @@ trait ParameterTrait
{
/**
* @param string|class-string|object|null $ref
* @param array<string,Examples> $examples
* @param array<Examples> $examples
* @param array<MediaType>|JsonContent|XmlContent|Attachable|null $content
* @param array<string,mixed>|null $x
* @param Attachable[]|null $attachables

View File

@ -6,6 +6,7 @@
namespace OpenApi\Attributes;
use OpenApi\Annotations\Examples;
use OpenApi\Generator;
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
@ -19,6 +20,7 @@ class Schema extends \OpenApi\Annotations\Schema
* @param int|float $maximum
* @param int|float $minimum
* @param array<string|int|float|bool|\UnitEnum|null>|class-string|null $enum
* @param array<Examples> $examples
* @param array<Schema|\OpenApi\Annotations\Schema> $allOf
* @param array<Schema|\OpenApi\Annotations\Schema> $anyOf
* @param array<Schema|\OpenApi\Annotations\Schema> $oneOf
@ -57,6 +59,7 @@ class Schema extends \OpenApi\Annotations\Schema
?Xml $xml = null,
?ExternalDocumentation $externalDocs = null,
mixed $example = Generator::UNDEFINED,
?array $examples = null,
?bool $nullable = null,
?bool $deprecated = null,
?array $allOf = null,
@ -105,7 +108,7 @@ class Schema extends \OpenApi\Annotations\Schema
'const' => $const,
'x' => $x ?? Generator::UNDEFINED,
'attachables' => $attachables ?? Generator::UNDEFINED,
'value' => $this->combine($items, $discriminator, $externalDocs, $attachables),
'value' => $this->combine($items, $discriminator, $externalDocs, $examples, $attachables),
]);
}
}

View File

@ -14,7 +14,7 @@ class XmlContent extends \OpenApi\Annotations\XmlContent
/**
* @param string|non-empty-array<string>|null $type
* @param string|class-string|object|null $ref
* @param array<string,Examples> $examples
* @param array<Examples> $examples
* @param string[] $required
* @param int|float $maximum
* @param int|float $minimum

View File

@ -144,7 +144,11 @@ class AugmentProperties implements ProcessorInterface
$property->minimum = 0;
} elseif ($type === 'non-zero-int') {
$property->type = 'integer';
$property->not = ['const' => 0];
if ($property->isOpenApiVersion(OA\OpenApi::VERSION_3_1_0)) {
$property->not = ['const' => 0];
} else {
$property->not = ['enum' => [0]];
}
}
}

View File

@ -11,7 +11,6 @@ use OpenApi\Analysers\AnnotationFactoryInterface;
use OpenApi\Analysers\AttributeAnnotationFactory;
use OpenApi\Analysers\DocBlockAnnotationFactory;
use OpenApi\Analysers\ReflectionAnalyser;
use OpenApi\Analysers\TokenAnalyser;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Context;
@ -184,9 +183,7 @@ class ReflectionAnalyserTest extends OpenApiTestCase
*/
public function testPhp8PromotedProperties(): void
{
if ($this->getAnalyzer() instanceof TokenAnalyser) {
$this->markTestSkipped();
}
$this->skipLegacy();
$analysis = $this->analysisFromFixtures(['PHP/Php8PromotedProperties.php']);
$schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true);

View File

@ -142,6 +142,23 @@ END;
$this->assertFalse((new OA\AdditionalProperties([]))->isRoot(OA\Schema::class));
$this->assertTrue((new SubSchema([]))->isRoot(OA\Schema::class));
}
/**
* @requires PHP 8.1
*/
public function testValidateExamples(): void
{
$this->skipLegacy();
$analysis = $this->analysisFromFixtures(['BadExampleParameter.php']);
$analysis->process((new Generator())->getProcessors());
$this->assertOpenApiLogEntryContains('Required @OA\PathItem() not found');
$this->assertOpenApiLogEntryContains('Required @OA\Info() not found');
$this->assertOpenApiLogEntryContains('"example" and "examples" are mutually exclusive');
$analysis->validate();
}
}
class SubSchema extends OA\Schema

View File

@ -64,13 +64,19 @@ class AttributesSyncTest extends OpenApiTestCase
}
if (!$found) {
// exclusions...
if ($attributeRC->isSubclassOf(OA\Operation::class) && 'method' == $propertyName) {
if ($attributeRC->isSubclassOf(OA\Operation::class) && in_array($propertyName, ['method'])) {
continue;
}
if ($attributeRC->isSubclassOf(OA\Attachable::class) && 'x' == $propertyName) {
if ($attributeRC->isSubclassOf(OA\Attachable::class) && in_array($propertyName, ['x'])) {
continue;
}
if ($attributeRC->isSubclassOf(OA\AdditionalProperties::class) && 'additionalProperties' == $propertyName) {
if ($attributeRC->isSubclassOf(OA\AdditionalProperties::class) && in_array($propertyName, ['additionalProperties', 'examples'])) {
continue;
}
if ($attributeRC->isSubclassOf(OA\Items::class) && in_array($propertyName, ['examples'])) {
continue;
}
if ($attributeRC->isSubclassOf(OA\Property::class) && in_array($propertyName, ['examples'])) {
continue;
}
if (in_array($propertyName, static::$SCHEMA_EXCLUSIONS)) {

View File

@ -17,7 +17,7 @@ class OpenApiTest extends OpenApiTestCase
$this->assertOpenApiLogEntryContains('Required @OA\Info() not found');
$openapi = new OA\OpenApi(['_context' => $this->getContext()]);
$openapi->openapi = '3.0.0';
$openapi->openapi = OA\OpenApi::VERSION_3_0_0;
$openapi->validate();
}
@ -26,7 +26,7 @@ class OpenApiTest extends OpenApiTestCase
$this->assertOpenApiLogEntryContains("At least one of 'Required @OA\PathItem(), @OA\Components() or @OA\Webhook() not found'");
$openapi = new OA\OpenApi(['_context' => $this->getContext()]);
$openapi->openapi = '3.1.0';
$openapi->openapi = OA\OpenApi::VERSION_3_1_0;
$openapi->validate();
}

View File

@ -12,7 +12,7 @@ use OpenApi\Tests\Fixtures\Attributes as OAF;
/**
* The Spec.
*/
#[OAT\OpenApi(openapi: '3.1.0', security: [['bearerAuth' => []]])]
#[OAT\OpenApi(openapi: OAT\OpenApi::VERSION_3_1_0, security: [['bearerAuth' => []]])]
#[OAT\Info(
version: '1.0.0',
title: 'Basic single file API',

View File

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Tests\Fixtures;
use OpenApi\Attributes as OAT;
#[OAT\QueryParameter(
name: 'bad',
example: 'not good',
examples: [new OAT\Examples(example: 'first', summary: 'First example', value: false)]
)]
class BadExampleParameter
{
}

View File

@ -0,0 +1,39 @@
openapi: 3.1.0
info:
title: 'Attribute Inheritance Scratch'
version: '1.0'
paths:
/api/endpoint:
get:
description: 'An endpoint'
operationId: 6e8c7c1a5488a11e2b1bf5f3b726c29c
responses:
'200':
description: OK
components:
schemas:
Base:
properties:
id:
type: integer
type: object
Child1:
type: object
allOf:
-
$ref: '#/components/schemas/Base'
-
properties:
name:
type: string
type: object
Child2:
type: object
allOf:
-
$ref: '#/components/schemas/Base'
-
properties:
title:
type: string
type: object

View File

@ -0,0 +1,18 @@
openapi: 3.1.0
info:
title: ClassRef
version: '1.0'
paths:
/endpoint:
get:
operationId: f835f4c3d03452d2e5d2f48b7c314e2f
responses:
'200':
description: 'All good'
content:
application/json:
schema:
$ref: '#/components/schemas/YoYo'
components:
schemas:
YoYo: { }

View File

@ -0,0 +1,63 @@
openapi: 3.1.0
info:
title: API
version: 1.0.0
paths:
/target_groups:
get:
tags:
- 'Target groups'
summary: 'List target groups'
operationId: groups
responses:
'200':
description: 'Successful response of [TargetGroupListDto]'
content:
application/json:
schema:
$ref: '#/components/schemas/TargetGroupListDto'
components:
schemas:
TargetGroupListDto:
title: TargetGroupListDto
required:
- targetGroups
properties:
targetGroups:
title: TargetGroupDto
type: array
items:
$ref: '#/components/schemas/TargetGroupDto'
type: object
TargetGroupDto:
title: TargetGroupDto
required:
- groupId
- groupName
- targets
properties:
groupId:
type: string
groupName:
type: string
targets:
title: TargetDto
type: array
items:
$ref: '#/components/schemas/TargetDto'
type: object
TargetDto:
title: TargetDto
required:
- targetId
- targetType
properties:
targetId:
$ref: '#/components/schemas/TargetId'
targetType:
$ref: '#/components/schemas/TargetType'
type: object
TargetId:
title: TargetId
TargetType:
title: TargetType

View File

@ -0,0 +1,15 @@
openapi: 3.1.0
info:
title: 'Custom Attribute Schema Scratch'
version: '1.0'
paths:
/api/endpoint:
get:
description: 'An endpoint'
operationId: da8efe0fc0b09040d0bda7cf58d9421d
responses:
'200':
description: OK
components:
schemas:
MyClass: { }

View File

@ -0,0 +1,29 @@
openapi: 3.1.0
info:
title: 'Extended Attributes Scratch'
version: '1.0'
paths:
/api/endpoint:
get:
description: 'An endpoint'
operationId: 242c3431636158b96b85e2adbefa76f4
responses:
'200':
description: OK
components:
schemas:
CAItemModel: { }
CAModel:
properties:
name:
type:
- string
- 'null'
item:
$ref: '#/components/schemas/CAItemModel'
items:
title: CAItemModel
type: array
items:
$ref: '#/components/schemas/CAItemModel'
type: object

View File

@ -0,0 +1,20 @@
openapi: 3.1.0
info:
title: Api
version: 1.0.0
paths:
/api:
get:
operationId: 232f976e7ad23d1e147e984eb3da3506
responses:
'200':
description: 'All good'
components:
schemas:
CustomPropertyAttribute:
properties:
number:
type:
- integer
- 'null'
type: object

View File

@ -9,8 +9,14 @@ namespace OpenApi\Tests\Fixtures\Scratch;
use OpenApi\Annotations as OA;
use OpenApi\Attributes as OAT;
/** @OA\Schema */
class DocblockSchema
/**
* @OA\OpenApi(
* openapi="3.0.0"
* )
*
* @OA\Schema
*/
class DocblocksSchema
{
/**
* @OA\Property
@ -76,7 +82,7 @@ class DocblockSchema
}
#[OAT\Schema]
class DocblockSchemaChild extends DocblockSchema
class DocblockSchemaChild extends DocblocksSchema
{
/** @var int The id */
#[OAT\Property]
@ -84,7 +90,7 @@ class DocblockSchemaChild extends DocblockSchema
}
/**
* @OA\Info(title="API", version="1.0")
* @OA\Info(title="Dockblocks", version="1.0")
* @OA\Get(
* path="/api/endpoint",
* @OA\Response(
@ -93,6 +99,6 @@ class DocblockSchemaChild extends DocblockSchema
* )
* )
*/
class DockblockEndpoint
class DockblocksEndpoint
{
}

View File

@ -0,0 +1,69 @@
openapi: 3.0.0
info:
title: Dockblocks
version: '1.0'
paths:
/api/endpoint:
get:
operationId: 2dd3513e31e559b14abab58814959d68
responses:
'200':
description: 'successful operation'
components:
schemas:
DocblocksSchema:
properties:
name:
description: 'The name'
type: string
oldName:
description: 'The name (old)'
type: string
deprecated: true
rangeInt:
description: 'The range integer'
type: integer
maximum: 25
minimum: 5
minRangeInt:
description: 'The minimum range integer'
type: integer
minimum: 2
maxRangeInt:
description: 'The maximum range integer'
type: integer
maximum: 10
positiveInt:
description: 'The positive integer'
type: integer
minimum: 1
negativeInt:
description: 'The negative integer'
type: integer
maximum: -1
nonPositiveInt:
description: 'The non-positive integer'
type: integer
maximum: 0
nonNegativeInt:
description: 'The non-negative integer'
type: integer
minimum: 0
nonZeroInt:
description: 'The non-zero integer'
type: integer
not:
enum:
- 0
type: object
DocblockSchemaChild:
type: object
allOf:
-
$ref: '#/components/schemas/DocblocksSchema'
-
properties:
id:
description: 'The id'
type: integer
type: object

View File

@ -1,17 +1,17 @@
openapi: 3.0.0
openapi: 3.1.0
info:
title: API
title: Dockblocks
version: '1.0'
paths:
/api/endpoint:
get:
operationId: deb4d2aa2d9e471e6e6a6c8ed61123d0
operationId: 2dd3513e31e559b14abab58814959d68
responses:
'200':
description: 'successful operation'
components:
schemas:
DocblockSchema:
DocblocksSchema:
properties:
name:
description: 'The name'
@ -23,8 +23,8 @@ components:
rangeInt:
description: 'The range integer'
type: integer
minimum: 5
maximum: 25
minimum: 5
minRangeInt:
description: 'The minimum range integer'
type: integer
@ -54,13 +54,12 @@ components:
type: integer
not:
const: 0
type: object
DocblockSchemaChild:
type: object
allOf:
-
$ref: '#/components/schemas/DocblockSchema'
$ref: '#/components/schemas/DocblocksSchema'
-
properties:
id:

View File

@ -0,0 +1,29 @@
openapi: 3.1.0
info:
title: DuplicateRef
version: '1.0'
paths:
/api/endpoint:
get:
description: 'An endpoint'
operationId: 3f9d6af187f46eeacb56ecc9ebbf5fba
responses:
'200':
description: OK
components:
schemas:
abstract-user:
properties:
name:
type: string
email:
type: string
type: object
create-user:
allOf:
-
$ref: '#/components/schemas/abstract-user'
-
required:
- name
- email

View File

@ -0,0 +1,59 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Tests\Fixtures\Scratch;
use OpenApi\Attributes as OAT;
#[OAT\Schema(
schema: 'YoYo',
examples: [
new OAT\Examples(
example: 'yo',
summary: 'the yo',
value: 'YoYo'
),
]
)]
class ExampleSchema
{
}
#[OAT\Info(title: 'Examples', version: '1.0')]
#[OAT\Get(
path: '/endpoint/{name}/{other}',
parameters: [
new OAT\PathParameter(
name: 'name',
required: true,
schema: new OAT\Schema(type: 'string'),
example: 'Fritz'
),
new OAT\PathParameter(
name: 'other',
required: true,
schema: new OAT\Schema(type: 'string'),
examples: [
new OAT\Examples(
example: 'o1',
summary: 'other example 1',
value: 'ping'
),
new OAT\Examples(
example: 'o2',
summary: 'other example 2',
value: 'pong'
),
]
),
],
responses: [
new OAT\Response(response: 200, description: 'OK'),
]
)]
class ExamplesEndpoint
{
}

View File

@ -0,0 +1,35 @@
openapi: 3.0.0
info:
title: Examples
version: '1.0'
paths:
'/endpoint/{name}/{other}':
get:
operationId: fc70869eba6848b340d6f3546b63e344
parameters:
-
name: name
in: path
required: true
schema:
type: string
example: Fritz
-
name: other
in: path
required: true
schema:
type: string
examples:
o1:
summary: 'other example 1'
value: ping
o2:
summary: 'other example 2'
value: pong
responses:
'200':
description: OK
components:
schemas:
YoYo: { }

View File

@ -0,0 +1,39 @@
openapi: 3.1.0
info:
title: Examples
version: '1.0'
paths:
'/endpoint/{name}/{other}':
get:
operationId: fc70869eba6848b340d6f3546b63e344
parameters:
-
name: name
in: path
required: true
schema:
type: string
example: Fritz
-
name: other
in: path
required: true
schema:
type: string
examples:
o1:
summary: 'other example 1'
value: ping
o2:
summary: 'other example 2'
value: pong
responses:
'200':
description: OK
components:
schemas:
YoYo:
examples:
yo:
summary: 'the yo'
value: YoYo

View File

@ -6,8 +6,8 @@
use OpenApi\Attributes as OAT;
#[OAT\Schema(schema: 'minMaxClass31')]
class MinMaxClass31
#[OAT\Schema(schema: 'minMaxClass')]
class MinMaxClass
{
#[OAT\Property(minimum: 10)]
private int $min = 10;
@ -25,12 +25,11 @@ class MinMaxClass31
private int $exclusiveMinMaxNumber = 61;
}
#[OAT\OpenApi(openapi: '3.1.0')]
#[OAT\Info(
title: 'Exclusive minimum and maximum',
version: '1.0'
)]
class ExclusiveMinMax31
class ExclusiveMinMax
{
#[OAT\Get(
path: '/api/endpoint',

View File

@ -0,0 +1,43 @@
openapi: 3.0.0
info:
title: 'Exclusive minimum and maximum'
version: '1.0'
paths:
/api/endpoint:
get:
description: 'An endpoint'
operationId: ef57acec977120506db6b2cf1c500c15
responses:
'200':
description: OK
components:
schemas:
minMaxClass:
properties:
min:
type: integer
minimum: 10
exclusiveMin:
type: integer
minimum: 20
exclusiveMinimum: true
max:
type: integer
maximum: 30
exclusiveMax:
type: integer
maximum: 40
exclusiveMaximum: true
exclusiveMinMax:
type: integer
maximum: 60
exclusiveMaximum: true
minimum: 50
exclusiveMinimum: true
exclusiveMinMaxNumber:
type: integer
exclusiveMaximum: true
exclusiveMinimum: true
minimum: 60
maximum: 70
type: object

View File

@ -6,13 +6,13 @@ paths:
/api/endpoint:
get:
description: 'An endpoint'
operationId: da2bbb06428a6f5ae9199a22a80436d7
operationId: ef57acec977120506db6b2cf1c500c15
responses:
'200':
description: OK
components:
schemas:
minMaxClass31:
minMaxClass:
properties:
min:
type: integer

View File

@ -0,0 +1,38 @@
openapi: 3.1.0
info:
title: API
version: '1.0'
paths:
/api/endpoint:
get:
operationId: f2c3bf0cddeaf2c12b195131fb1d4e5f
responses:
'200':
description: 'successful operation'
content:
application/json:
schema:
$ref: '#/components/schemas/Address'
components:
schemas:
Address:
required:
- street
properties:
created_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
id:
type: integer
format: int64
readOnly: true
street:
type: string
type: object
xml:
name: Address

View File

@ -0,0 +1,55 @@
openapi: 3.1.0
info:
title: API
version: '1.0'
paths:
/api/endpoint:
get:
operationId: 7bc218b8e28854bc705f3b582ab21668
responses:
'200':
description: 'successful operation'
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
components:
schemas:
ModelExtended:
description: 'This model can be ignored, it is just used for inheritance.'
properties:
id:
type: integer
format: int64
readOnly: true
created_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
type: object
Product:
description: Product
required:
- number
- name
type: object
xml:
name: Product
allOf:
-
$ref: '#/components/schemas/ModelExtended'
-
properties:
deleted_at:
type: string
format: date-time
readOnly: true
number:
type: string
name:
type: string
type: object

View File

@ -0,0 +1,33 @@
openapi: 3.1.0
info:
title: 'Multiple Paths For Endpoint Scratch'
version: '1.0'
paths:
/api/class/endpoint:
get:
description: 'A class endpoint'
operationId: f6fa254544c561ced460509c98adbc8b
responses:
'200':
description: OK
/api/class/endpoint2:
get:
description: 'Another class endpoint'
operationId: 7816e3a6652e1e4782355ec5675e75f8
responses:
'200':
description: OK
/api/method/endpoint:
get:
description: 'A method endpoint'
operationId: b134e3908c5787b578f7faaa63283168
responses:
'200':
description: OK
/api/method/endpoint2:
get:
description: 'Another method endpoint'
operationId: ff3758f75b33d1e680c47e92ed39844f
responses:
'200':
description: OK

View File

@ -0,0 +1,19 @@
openapi: 3.1.0
info:
title: 'Nested Additional Properties'
version: '1.0'
paths:
/api/endpoint:
get:
description: 'An endpoint'
operationId: b81d5bbc60cb1ce6c15eb67081315573
responses:
'200':
description: OK
components:
schemas:
NestedAdditionalAttributes:
type: object
additionalProperties:
additionalProperties:
type: string

View File

@ -0,0 +1,30 @@
openapi: 3.1.0
info:
title: 'Parameter Content Scratch'
version: '1.0'
paths:
/api/endpoint:
get:
operationId: b870e13084c8a89270151bb3e43f215a
responses:
'200':
description: OK
components:
schemas:
NestedSchema:
required:
- errors
properties:
errors:
description: 'Validation errors'
type: object
minItems: 1
uniqueItems: true
additionalProperties:
description: 'Array of error messages for property'
type: array
items:
type: string
minItems: 1
uniqueItems: true
type: object

View File

@ -1,57 +0,0 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
use OpenApi\Attributes as OAT;
#[OAT\Schema(schema: 'repository')]
class Repository31
{
}
#[OAT\OpenApi(openapi: '3.1.0')]
#[OAT\Info(
title: 'Null Ref',
version: '1.0'
)]
class NullRef31
{
#[OAT\Get(
path: '/api/refonly',
operationId: 'refonly',
responses: [
new OAT\Response(
response: 200,
description: 'Ref response',
content: new OAT\JsonContent(
ref: '#/components/schemas/repository',
nullable: true
)
),
]
)]
public function refonly()
{
}
#[OAT\Get(
path: '/api/refplus',
operationId: 'refplus',
responses: [
new OAT\Response(
response: 200,
description: 'Ref plus response',
content: new OAT\JsonContent(
ref: '#/components/schemas/repository',
description: 'The repository',
nullable: true
)
),
]
)]
public function refplusy()
{
}
}

View File

@ -8,7 +8,6 @@ namespace OpenApi\Tests\Fixtures\Scratch;
use OpenApi\Attributes as OAT;
#[OAT\OpenApi(openapi: '3.1.0')]
#[OAT\Info(title: 'Nullable', version: '1.0')]
class Api
{

View File

@ -0,0 +1,57 @@
openapi: 3.0.0
info:
title: Nullable
version: '1.0'
paths:
/api/endpoint:
get:
description: 'An endpoint'
operationId: 4608e00dd49333806891310fdc161547
responses:
'200':
description: OK
components:
schemas:
MyDateTime:
type: string
format: rfc3339-timestamp
externalDocs:
description: '**RFC3339** IETF'
url: 'https://tools.ietf.org/html/rfc3339'
example: '2023-08-02T07:06:46+03:30'
Nullable:
properties:
firstname:
type: string
nullable: true
lastname:
type: string
nullable: true
birthdate:
nullable: true
oneOf:
-
$ref: '#/components/schemas/MyDateTime'
otherdate:
nullable: true
oneOf:
-
$ref: '#/components/schemas/MyDateTime'
anotherdate:
nullable: true
oneOf:
-
$ref: '#/components/schemas/MyDateTime'
description:
type:
- string
- 'null'
choice:
type: string
enum:
- Choice1
- Choice2
- null
example: Choice1
nullable: true
type: object

View File

@ -0,0 +1,26 @@
openapi: 3.1.0
info:
title: 'Parameter Content Scratch'
version: '1.0'
paths:
/api/endpoint:
get:
tags:
- endpoints
summary: 'An API endpoint.'
description: 'An endpoint'
operationId: endpoint
parameters:
-
name: filter
in: query
content:
application/json:
schema:
properties:
type: { type: string }
color: { type: string }
type: object
responses:
'200':
description: OK

View File

@ -0,0 +1,26 @@
openapi: 3.1.0
info:
title: 'Promoted Property Description Scratch'
version: '1.0'
paths:
/api/endpoint:
get:
description: 'An endpoint'
operationId: ff3eaf9da24371c37532fd0de82a3212
responses:
'200':
description: OK
components:
schemas:
PromotedPropertyDescription:
properties:
thevalue:
description: 'Property value.'
type: string
thename:
description: 'Property name.'
type: string
themeta:
description: 'Property meta.'
type: string
type: object

View File

@ -0,0 +1,22 @@
openapi: 3.1.0
info:
title: API
version: '1.0'
paths:
/api/endpoint:
get:
operationId: 6e9f788346a2235f83520975c9ba68c8
responses:
'200':
description: 'successful operation'
content:
application/json:
schema:
$ref: '#/components/schemas/PropertyInheritance'
components:
schemas:
PropertyInheritance:
properties:
inheritedfilter:
type: string
type: object

View File

@ -0,0 +1,35 @@
openapi: 3.1.0
info:
title: RequestBody
version: '1.0'
paths:
/endpoint:
get:
operationId: 30597cf43b480393042f6b01a9d7980c
requestBody:
description: 'Information about a new pet in the system'
content:
application/json: { }
responses:
'200':
description: 'All good'
/endpoint/ref:
post:
operationId: 4250dd87e4e3872a8f2e481532cbc245
requestBody:
$ref: '#/components/requestBodies/RequestBodyRef'
responses:
'200':
description: 'All good'
/endpoint/ref-foo:
post:
operationId: 344406e28927343e4e9e4f39bd6c385b
requestBody:
$ref: '#/components/requestBodies/foo'
responses:
'200':
description: 'All good'
components:
requestBodies:
RequestBodyRef: { }
foo: { }

View File

@ -0,0 +1,18 @@
openapi: 3.1.0
info:
title: API
version: '1.0'
paths:
/api/endpoint:
get:
operationId: d5f395c872bfd6677e6c30c55c7d5463
responses:
'200':
description: 'successful operation'
components:
schemas:
SomeParent: { }
Child:
allOf:
-
$ref: '#/components/schemas/SomeParent'

View File

@ -9,7 +9,7 @@ namespace OpenApi\Tests\Fixtures\Scratch;
use OpenApi\Attributes as OAT;
#[OAT\Schema]
class Types31
class Types
{
#[OAT\Property(type: ['string', 'integer'])]
public string|int $stringInteger = '';
@ -18,7 +18,7 @@ class Types31
public mixed $massiveTypes = '';
}
#[OAT\OpenApi(openapi: '3.1.0')]
#[OAT\OpenApi(openapi: OAT\OpenApi::VERSION_3_1_0)]
#[OAT\Info(
title: 'List of types',
version: '1.0'
@ -28,6 +28,6 @@ class Types31
description: 'An endpoint',
responses: [new OAT\Response(response: 200, description: 'OK')]
)]
class Types31Endpoint
class TypesEndpoint
{
}

View File

@ -6,13 +6,13 @@ paths:
/api/endpoint:
get:
description: 'An endpoint'
operationId: 20a99cdde4e09b297c7555321c7cdb6f
operationId: 04621eecf5d57d633b0be3e42a30a379
responses:
'200':
description: OK
components:
schemas:
Types31:
Types:
properties:
stringInteger:
type:

View File

@ -8,7 +8,7 @@ namespace OpenApi\Tests\Fixtures\Scratch;
use OpenApi\Attributes as OAT;
#[OAT\PathParameter(name: 'itemName', description: 'The item name')]
#[OAT\PathParameter(name: 'item_name', description: 'The item name', required: true, schema: new OAT\Schema(type: 'string'))]
class UsingRefsParameter
{
}
@ -22,7 +22,7 @@ class UsingRefsResponse
#[OAT\Get(
path: '/item/{item_name}',
parameters: [
new OAT\Parameter(ref: '#/components/parameters/itemName'),
new OAT\Parameter(ref: '#/components/parameters/item_name'),
],
responses: [
new OAT\Response(response: 200, ref: '#/components/responses/item'),

View File

@ -8,7 +8,7 @@ paths:
operationId: 6ecb3788642c6ba8ce8d99cbcd554dbe
parameters:
-
$ref: '#/components/parameters/itemName'
$ref: '#/components/parameters/item_name'
responses:
'200':
$ref: '#/components/responses/item'
@ -17,7 +17,10 @@ components:
item:
description: 'Item response'
parameters:
itemName:
name: itemName
item_name:
name: item_name
in: path
description: 'The item name'
required: true
schema:
type: string

View File

@ -0,0 +1,26 @@
openapi: 3.1.0
info:
title: 'Parameter Ref'
version: 1.0.0
paths:
'/item/{item_name}':
get:
operationId: 6ecb3788642c6ba8ce8d99cbcd554dbe
parameters:
-
$ref: '#/components/parameters/item_name'
responses:
'200':
$ref: '#/components/responses/item'
components:
responses:
item:
description: 'Item response'
parameters:
item_name:
name: item_name
in: path
description: 'The item name'
required: true
schema:
type: string

View File

@ -16,6 +16,7 @@ use OpenApi\Annotations as OA;
use OpenApi\Context;
use OpenApi\Analysers\TokenAnalyser;
use OpenApi\Generator;
use OpenApi\Processors\MergeIntoOpenApi;
use PHPUnit\Framework\TestCase;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;
@ -84,6 +85,13 @@ class OpenApiTestCase extends TestCase
};
}
public function skipLegacy(): void
{
if ($this->getAnalyzer() instanceof TokenAnalyser) {
$this->markTestSkipped();
}
}
public function getContext(array $properties = [], ?string $version = OA\OpenApi::DEFAULT_VERSION): Context
{
return new Context(
@ -235,7 +243,8 @@ class OpenApiTestCase extends TestCase
(new Generator($this->getTrackingLogger()))
->setAnalyser($analyzer ?: $this->getAnalyzer())
->setProcessors($processors)
// run at least MergeIntoOpenApi to have a valid OpenApi version set
->setProcessors($processors ?: [new MergeIntoOpenApi()])
->generate($this->fixtures($files), $analysis, false);
return $analysis;

View File

@ -6,7 +6,6 @@
namespace OpenApi\Tests\Processors;
use OpenApi\Analysers\TokenAnalyser;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
use OpenApi\Processors\ExpandEnums;
@ -17,15 +16,16 @@ use OpenApi\Tests\Fixtures\PHP\Enums\StatusEnumStringBacked;
use OpenApi\Tests\Fixtures\PHP\Enums\TypeEnumStringBacked;
use OpenApi\Tests\OpenApiTestCase;
/**
* @requires PHP 8.1
*/
class ExpandEnumsTest extends OpenApiTestCase
{
public function setUp(): void
{
parent::setUp();
if (PHP_VERSION_ID < 80100 || $this->getAnalyzer() instanceof TokenAnalyser) {
$this->markTestSkipped();
}
$this->skipLegacy();
}
public function testExpandUnitEnum(): void

View File

@ -6,6 +6,7 @@
namespace OpenApi\Tests;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
class ScratchTest extends OpenApiTestCase
@ -19,11 +20,27 @@ class ScratchTest extends OpenApiTestCase
continue;
}
yield $name => [
$this->fixture("Scratch/$name.php"),
$this->fixture("Scratch/$name.yaml"),
[],
$scratch = $this->fixture("Scratch/$name.php");
$specs = [
$this->fixture("Scratch/{$name}3.1.0.yaml") => OA\OpenApi::VERSION_3_1_0,
$this->fixture("Scratch/{$name}3.0.0.yaml") => OA\OpenApi::VERSION_3_0_0,
];
$expectedLogs = [
'Examples-3.0.0' => ['@OA\Schema() is only allowed for 3.1.0'],
];
foreach ($specs as $spec => $version) {
if (file_exists($spec)) {
$dataSet = "$name-$version";
yield $dataSet => [
$scratch,
$spec,
$version,
array_key_exists($dataSet, $expectedLogs) ? $expectedLogs[$dataSet] : [],
];
}
}
}
}
@ -34,21 +51,19 @@ class ScratchTest extends OpenApiTestCase
*
* @requires PHP 8.1
*/
public function testScratch(string $scratch, string $spec, array $expectedLog): void
public function testScratch(string $scratch, string $spec, string $version, array $expectedLogs): void
{
foreach ($expectedLog as $logLine) {
foreach ($expectedLogs as $logLine) {
$this->assertOpenApiLogEntryContains($logLine);
}
require_once $scratch;
$openapi = (new Generator($this->getTrackingLogger()))
->setVersion($version)
->generate([$scratch]);
if (!file_exists($spec)) {
file_put_contents($spec, $openapi->toYaml());
}
// file_put_contents($spec, $openapi->toYaml());
$this->assertSpecEquals($openapi, file_get_contents($spec));
}
}

View File

@ -49,7 +49,7 @@ class SerializerTest extends OpenApiTestCase
$path->post->responses = [$resp, $respRange];
$expected = new OA\OpenApi(['_context' => $this->getContext()]);
$expected->openapi = '3.0.0';
$expected->openapi = OA\OpenApi::VERSION_3_0_0;
$expected->paths = [
$path,
];