diff --git a/docs/reference/attributes.md b/docs/reference/attributes.md index 67fe301cd..b805b813b 100644 --- a/docs/reference/attributes.md +++ b/docs/reference/attributes.md @@ -190,16 +190,42 @@ list.

Required:

No details available.

Required:no
contains : array|null

No details available.

Required:no
+
minContains : int|null
+

@since OpenAPI 3.1.0

Required:no
+
maxContains : int|null
+

@since OpenAPI 3.1.0

Required:no
+
prefixItems : array|null
+

@since OpenAPI 3.1.0

Required:no
patternProperties : array|null

No details available.

Required:no
unevaluatedProperties : array|null

No details available.

Required:no
+
unevaluatedItems : mixed|null
+

@since OpenAPI 3.1.0

Required:no
dependencies : mixed|null

No details available.

Required:no
+
dependentRequired : array|null
+

@since OpenAPI 3.1.0

Required:no
+
dependentSchemas : array|null
+

@since OpenAPI 3.1.0

Required:no
propertyNames : mixed|null

No details available.

Required:no
const : mixed|null

@since OpenAPI 3.1.0

Required:no
+
if : mixed|null
+

@since OpenAPI 3.1.0

Required:no
+
then : mixed|null
+

Applied when `if` succeeds.
+
+@since OpenAPI 3.1.0

Required:no
+
else : mixed|null
+

Applied when `if` fails.
+
+@since OpenAPI 3.1.0

Required:no
+
contentSchema : mixed|null
+

Schema for the decoded content described by `contentMediaType`.
+
+@since OpenAPI 3.1.0

Required:no
x : array<string,mixed>|null

While the OpenAPI Specification tries to accommodate most use cases, additional data can be added to extend the specification at certain points.
For further details see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#specificationExtensions
@@ -1233,16 +1259,42 @@ list.

Required:

No details available.

Required:no
contains : array|null

No details available.

Required:no
+
minContains : int|null
+

@since OpenAPI 3.1.0

Required:no
+
maxContains : int|null
+

@since OpenAPI 3.1.0

Required:no
+
prefixItems : array|null
+

@since OpenAPI 3.1.0

Required:no
patternProperties : array|null

No details available.

Required:no
unevaluatedProperties : array|null

No details available.

Required:no
+
unevaluatedItems : mixed|null
+

@since OpenAPI 3.1.0

Required:no
dependencies : mixed|null

No details available.

Required:no
+
dependentRequired : array|null
+

@since OpenAPI 3.1.0

Required:no
+
dependentSchemas : array|null
+

@since OpenAPI 3.1.0

Required:no
propertyNames : mixed|null

No details available.

Required:no
const : mixed|null

@since OpenAPI 3.1.0

Required:no
+
if : mixed|null
+

@since OpenAPI 3.1.0

Required:no
+
then : mixed|null
+

Applied when `if` succeeds.
+
+@since OpenAPI 3.1.0

Required:no
+
else : mixed|null
+

Applied when `if` fails.
+
+@since OpenAPI 3.1.0

Required:no
+
contentSchema : mixed|null
+

Schema for the decoded content described by `contentMediaType`.
+
+@since OpenAPI 3.1.0

Required:no
x : array<string,mixed>|null

While the OpenAPI Specification tries to accommodate most use cases, additional data can be added to extend the specification at certain points.
For further details see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#specificationExtensions
@@ -1445,16 +1497,42 @@ list.

Required:

No details available.

Required:no
contains : array|null

No details available.

Required:no
+
minContains : int|null
+

@since OpenAPI 3.1.0

Required:no
+
maxContains : int|null
+

@since OpenAPI 3.1.0

Required:no
+
prefixItems : array|null
+

@since OpenAPI 3.1.0

Required:no
patternProperties : array|null

No details available.

Required:no
unevaluatedProperties : array|null

No details available.

Required:no
+
unevaluatedItems : mixed|null
+

@since OpenAPI 3.1.0

Required:no
dependencies : mixed|null

No details available.

Required:no
+
dependentRequired : array|null
+

@since OpenAPI 3.1.0

Required:no
+
dependentSchemas : array|null
+

@since OpenAPI 3.1.0

Required:no
propertyNames : mixed|null

No details available.

Required:no
const : mixed|null

@since OpenAPI 3.1.0

Required:no
+
if : mixed|null
+

@since OpenAPI 3.1.0

Required:no
+
then : mixed|null
+

Applied when `if` succeeds.
+
+@since OpenAPI 3.1.0

Required:no
+
else : mixed|null
+

Applied when `if` fails.
+
+@since OpenAPI 3.1.0

Required:no
+
contentSchema : mixed|null
+

Schema for the decoded content described by `contentMediaType`.
+
+@since OpenAPI 3.1.0

Required:no
x : array<string,mixed>|null

While the OpenAPI Specification tries to accommodate most use cases, additional data can be added to extend the specification at certain points.
For further details see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#specificationExtensions
@@ -2364,16 +2442,42 @@ list.

Required:

No details available.

Required:no
contains : array|null

No details available.

Required:no
+
minContains : int|null
+

@since OpenAPI 3.1.0

Required:no
+
maxContains : int|null
+

@since OpenAPI 3.1.0

Required:no
+
prefixItems : array|null
+

@since OpenAPI 3.1.0

Required:no
patternProperties : array|null

No details available.

Required:no
unevaluatedProperties : array|null

No details available.

Required:no
+
unevaluatedItems : mixed|null
+

@since OpenAPI 3.1.0

Required:no
dependencies : mixed|null

No details available.

Required:no
+
dependentRequired : array|null
+

@since OpenAPI 3.1.0

Required:no
+
dependentSchemas : array|null
+

@since OpenAPI 3.1.0

Required:no
propertyNames : mixed|null

No details available.

Required:no
const : mixed|null

@since OpenAPI 3.1.0

Required:no
+
if : mixed|null
+

@since OpenAPI 3.1.0

Required:no
+
then : mixed|null
+

Applied when `if` succeeds.
+
+@since OpenAPI 3.1.0

Required:no
+
else : mixed|null
+

Applied when `if` fails.
+
+@since OpenAPI 3.1.0

Required:no
+
contentSchema : mixed|null
+

Schema for the decoded content described by `contentMediaType`.
+
+@since OpenAPI 3.1.0

Required:no
x : array<string,mixed>|null

While the OpenAPI Specification tries to accommodate most use cases, additional data can be added to extend the specification at certain points.
For further details see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#specificationExtensions
@@ -2937,16 +3041,42 @@ list.

Required:

No details available.

Required:no
contains : array|null

No details available.

Required:no
+
minContains : int|null
+

@since OpenAPI 3.1.0

Required:no
+
maxContains : int|null
+

@since OpenAPI 3.1.0

Required:no
+
prefixItems : array|null
+

@since OpenAPI 3.1.0

Required:no
patternProperties : array|null

No details available.

Required:no
unevaluatedProperties : array|null

No details available.

Required:no
+
unevaluatedItems : mixed|null
+

@since OpenAPI 3.1.0

Required:no
dependencies : mixed|null

No details available.

Required:no
+
dependentRequired : array|null
+

@since OpenAPI 3.1.0

Required:no
+
dependentSchemas : array|null
+

@since OpenAPI 3.1.0

Required:no
propertyNames : mixed|null

No details available.

Required:no
const : mixed|null

@since OpenAPI 3.1.0

Required:no
+
if : mixed|null
+

@since OpenAPI 3.1.0

Required:no
+
then : mixed|null
+

Applied when `if` succeeds.
+
+@since OpenAPI 3.1.0

Required:no
+
else : mixed|null
+

Applied when `if` fails.
+
+@since OpenAPI 3.1.0

Required:no
+
contentSchema : mixed|null
+

Schema for the decoded content described by `contentMediaType`.
+
+@since OpenAPI 3.1.0

Required:no
x : array<string,mixed>|null

While the OpenAPI Specification tries to accommodate most use cases, additional data can be added to extend the specification at certain points.
For further details see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#specificationExtensions
@@ -3483,16 +3613,42 @@ list.

Required:

No details available.

Required:no
contains : array|null

No details available.

Required:no
+
minContains : int|null
+

@since OpenAPI 3.1.0

Required:no
+
maxContains : int|null
+

@since OpenAPI 3.1.0

Required:no
+
prefixItems : array|null
+

@since OpenAPI 3.1.0

Required:no
patternProperties : array|null

No details available.

Required:no
unevaluatedProperties : array|null

No details available.

Required:no
+
unevaluatedItems : mixed|null
+

@since OpenAPI 3.1.0

Required:no
dependencies : mixed|null

No details available.

Required:no
+
dependentRequired : array|null
+

@since OpenAPI 3.1.0

Required:no
+
dependentSchemas : array|null
+

@since OpenAPI 3.1.0

Required:no
propertyNames : mixed|null

No details available.

Required:no
const : mixed|null

@since OpenAPI 3.1.0

Required:no
+
if : mixed|null
+

@since OpenAPI 3.1.0

Required:no
+
then : mixed|null
+

Applied when `if` succeeds.
+
+@since OpenAPI 3.1.0

Required:no
+
else : mixed|null
+

Applied when `if` fails.
+
+@since OpenAPI 3.1.0

Required:no
+
contentSchema : mixed|null
+

Schema for the decoded content described by `contentMediaType`.
+
+@since OpenAPI 3.1.0

Required:no
x : array<string,mixed>|null

While the OpenAPI Specification tries to accommodate most use cases, additional data can be added to extend the specification at certain points.
For further details see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#specificationExtensions
diff --git a/docs/reference/spec-attributes.md b/docs/reference/spec-attributes.md index d238b3cdc..198a0b924 100644 --- a/docs/reference/spec-attributes.md +++ b/docs/reference/spec-attributes.md @@ -1523,6 +1523,8 @@ without it the schema has no component key and is reported as missing one.

The media type of string content encoding

contentEncoding : string|null

The encoding used for string content (e.g. base64)

+
contentSchema : Schema|null
+

Schema for the decoded content described by contentMediaType

minimum : int|float|null

Minimum numeric value (inclusive)

maximum : int|float|null
@@ -1657,6 +1659,8 @@ schemas with constrained additional properties:

The media type of string content encoding

contentEncoding : string|null

The encoding used for string content (e.g. base64)

+
contentSchema : Schema|null
+

Schema for the decoded content described by contentMediaType

minimum : int|float|null

Minimum numeric value (inclusive)

maximum : int|float|null
@@ -1799,6 +1803,8 @@ Since Items extends Schema, the implicit `OA\Property` shortcut applies — no e

The media type of string content encoding

contentEncoding : string|null

The encoding used for string content (e.g. base64)

+
contentSchema : Schema|null
+

Schema for the decoded content described by contentMediaType

minimum : int|float|null

Minimum numeric value (inclusive)

maximum : int|float|null
diff --git a/src/Annotations/AbstractAnnotation.php b/src/Annotations/AbstractAnnotation.php index 760681ef3..eac1c5420 100644 --- a/src/Annotations/AbstractAnnotation.php +++ b/src/Annotations/AbstractAnnotation.php @@ -111,6 +111,9 @@ public function __construct(array $properties) $nestedContext = new Context(['nested' => $this], $this->_context); foreach ($properties as $property => $value) { if (property_exists($this, $property)) { + if ($value instanceof AbstractAnnotation) { + $value = $this->nested($value, $nestedContext); + } $this->{$property} = $value; if (is_array($value)) { foreach ($value as $key => $annotation) { diff --git a/src/Annotations/JsonSchemaTrait.php b/src/Annotations/JsonSchemaTrait.php index 7acca12c9..efd62c2ac 100644 --- a/src/Annotations/JsonSchemaTrait.php +++ b/src/Annotations/JsonSchemaTrait.php @@ -135,6 +135,24 @@ trait JsonSchemaTrait */ public $contains = Undefined::UNDEFINED; + /** + * @var int + * @since OpenAPI 3.1.0 + */ + public $minContains = Undefined::UNDEFINED; + + /** + * @var int + * @since OpenAPI 3.1.0 + */ + public $maxContains = Undefined::UNDEFINED; + + /** + * @var array + * @since OpenAPI 3.1.0 + */ + public $prefixItems = Undefined::UNDEFINED; + /** * @var array */ @@ -145,11 +163,29 @@ trait JsonSchemaTrait */ public $unevaluatedProperties = Undefined::UNDEFINED; + /** + * @var mixed + * @since OpenAPI 3.1.0 + */ + public $unevaluatedItems = Undefined::UNDEFINED; + /** * @var mixed */ public $dependencies = Undefined::UNDEFINED; + /** + * @var array + * @since OpenAPI 3.1.0 + */ + public $dependentRequired = Undefined::UNDEFINED; + + /** + * @var array + * @since OpenAPI 3.1.0 + */ + public $dependentSchemas = Undefined::UNDEFINED; + /** * @var mixed */ @@ -160,6 +196,36 @@ trait JsonSchemaTrait * @since OpenAPI 3.1.0 */ public $const = Undefined::UNDEFINED; + + /** + * @var mixed + * @since OpenAPI 3.1.0 + */ + public $if = Undefined::UNDEFINED; + + /** + * Applied when `if` succeeds. + * + * @var mixed + * @since OpenAPI 3.1.0 + */ + public $then = Undefined::UNDEFINED; + + /** + * Applied when `if` fails. + * + * @var mixed + * @since OpenAPI 3.1.0 + */ + public $else = Undefined::UNDEFINED; + + /** + * Schema for the decoded content described by `contentMediaType`. + * + * @var mixed + * @since OpenAPI 3.1.0 + */ + public $contentSchema = Undefined::UNDEFINED; } /* @@ -189,11 +255,21 @@ trait JsonSchemaTrait bool|AdditionalProperties|null $additionalProperties = null, array|null $additionalItems = null, array|null $contains = null, + int|null $minContains = null, + int|null $maxContains = null, + array|null $prefixItems = null, array|null $patternProperties = null, array|null $unevaluatedProperties = null, + mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, + array|null $dependentRequired = null, + array|null $dependentSchemas = null, mixed $propertyNames = Undefined::UNDEFINED, mixed $const = Undefined::UNDEFINED, + mixed $if = Undefined::UNDEFINED, + mixed $then = Undefined::UNDEFINED, + mixed $else = Undefined::UNDEFINED, + mixed $contentSchema = Undefined::UNDEFINED, ** PARENT-PARAMS: @@ -214,10 +290,20 @@ trait JsonSchemaTrait 'additionalProperties' => $additionalProperties ?? Undefined::UNDEFINED, 'additionalItems' => $additionalItems ?? Undefined::UNDEFINED, 'contains' => $contains ?? Undefined::UNDEFINED, + 'minContains' => $minContains ?? Undefined::UNDEFINED, + 'maxContains' => $maxContains ?? Undefined::UNDEFINED, + 'prefixItems' => $prefixItems ?? Undefined::UNDEFINED, 'patternProperties' => $patternProperties ?? Undefined::UNDEFINED, 'unevaluatedProperties' => $unevaluatedProperties ?? Undefined::UNDEFINED, + 'unevaluatedItems' => $unevaluatedItems, 'dependencies' => $dependencies, + 'dependentRequired' => $dependentRequired ?? Undefined::UNDEFINED, + 'dependentSchemas' => $dependentSchemas ?? Undefined::UNDEFINED, 'propertyNames' => $propertyNames, 'const' => $const, + 'if' => $if, + 'then' => $then, + 'else' => $else, + 'contentSchema' => $contentSchema, */ diff --git a/src/Annotations/Schema.php b/src/Annotations/Schema.php index 51bb4f6ca..46bfb8163 100644 --- a/src/Annotations/Schema.php +++ b/src/Annotations/Schema.php @@ -312,6 +312,9 @@ class Schema extends AbstractAnnotation 'allOf' => '[' . Schema::class . ']', 'oneOf' => '[' . Schema::class . ']', 'anyOf' => '[' . Schema::class . ']', + 'prefixItems' => '[' . Schema::class . ']', + 'minContains' => 'integer', + 'maxContains' => 'integer', 'contentEncoding' => 'string', 'contentMediaType' => 'string', ]; @@ -365,6 +368,14 @@ public function jsonSerialize(): \stdClass if ($this->_context->isVersion('3.0.x')) { unset($data->examples); + foreach ([ + 'contains', 'minContains', 'maxContains', 'prefixItems', + 'patternProperties', 'propertyNames', 'unevaluatedProperties', 'unevaluatedItems', + 'dependentRequired', 'dependentSchemas', 'if', 'then', 'else', + 'contentMediaType', 'contentEncoding', 'contentSchema', + ] as $keyword) { + unset($data->{$keyword}); + } if (isset($data->const)) { $data->enum = [$data->const]; unset($data->const); @@ -388,7 +399,10 @@ public function validate(?Analysis $analysis = null, string $version = OpenApi:: { $isValid = parent::validate($analysis, $version, $context); - if ($this->hasType('array') && Undefined::isDefault($this->items)) { + if ($this->hasType('array') && Undefined::isDefault($this->items) + && (OpenApi::versionMatch($version, '3.0.x') + || (Undefined::isDefault($this->prefixItems) && Undefined::isDefault($this->contains)))) { + // 3.1 array schemas may describe their items via prefixItems or contains; 3.0 requires items $this->_context->logger->warning('@OA\\Items() is required when ' . $this->identity() . ' has type "array" in ' . $this->_context); $isValid = false; @@ -399,6 +413,18 @@ public function validate(?Analysis $analysis = null, string $version = OpenApi:: $this->_context->logger->warning(static::shorten(static::class) . '::examples is only allowed as of 3.1.0 in ' . $this->_context); $isValid = false; } + + // the same keywords the spec compiler warns about; the rest drop silently + foreach (['prefixItems', 'unevaluatedProperties', 'unevaluatedItems'] as $keyword) { + if (!Undefined::isDefault($this->{$keyword})) { + $this->_context->logger->warning($this->identity() . ': ' . $keyword . ' is not supported in OpenAPI 3.0 in ' . $this->_context); + $isValid = false; + } + } + if (!Undefined::isDefault($this->if) || !Undefined::isDefault($this->then) || !Undefined::isDefault($this->else)) { + $this->_context->logger->warning($this->identity() . ': if/then/else is not supported in OpenAPI 3.0 in ' . $this->_context); + $isValid = false; + } } // the keyword takes values, so an annotation other than `@OA\Examples` is serialised whole diff --git a/src/Attributes/AdditionalProperties.php b/src/Attributes/AdditionalProperties.php index fec93a888..29e263919 100644 --- a/src/Attributes/AdditionalProperties.php +++ b/src/Attributes/AdditionalProperties.php @@ -71,11 +71,21 @@ public function __construct( bool|AdditionalProperties|null $additionalProperties = null, array|null $additionalItems = null, array|null $contains = null, + int|null $minContains = null, + int|null $maxContains = null, + array|null $prefixItems = null, array|null $patternProperties = null, array|null $unevaluatedProperties = null, + mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, + array|null $dependentRequired = null, + array|null $dependentSchemas = null, mixed $propertyNames = Undefined::UNDEFINED, mixed $const = Undefined::UNDEFINED, + mixed $if = Undefined::UNDEFINED, + mixed $then = Undefined::UNDEFINED, + mixed $else = Undefined::UNDEFINED, + mixed $contentSchema = Undefined::UNDEFINED, // abstract annotation ?array $x = null, @@ -123,11 +133,21 @@ public function __construct( 'additionalProperties' => $additionalProperties ?? Undefined::UNDEFINED, 'additionalItems' => $additionalItems ?? Undefined::UNDEFINED, 'contains' => $contains ?? Undefined::UNDEFINED, + 'minContains' => $minContains ?? Undefined::UNDEFINED, + 'maxContains' => $maxContains ?? Undefined::UNDEFINED, + 'prefixItems' => $prefixItems ?? Undefined::UNDEFINED, 'patternProperties' => $patternProperties ?? Undefined::UNDEFINED, 'unevaluatedProperties' => $unevaluatedProperties ?? Undefined::UNDEFINED, + 'unevaluatedItems' => $unevaluatedItems, 'dependencies' => $dependencies, + 'dependentRequired' => $dependentRequired ?? Undefined::UNDEFINED, + 'dependentSchemas' => $dependentSchemas ?? Undefined::UNDEFINED, 'propertyNames' => $propertyNames, 'const' => $const, + 'if' => $if, + 'then' => $then, + 'else' => $else, + 'contentSchema' => $contentSchema, // abstract annotation 'x' => $x ?? Undefined::UNDEFINED, diff --git a/src/Attributes/Items.php b/src/Attributes/Items.php index b6e3ec057..fdffd7b13 100644 --- a/src/Attributes/Items.php +++ b/src/Attributes/Items.php @@ -71,11 +71,21 @@ public function __construct( bool|AdditionalProperties|null $additionalProperties = null, array|null $additionalItems = null, array|null $contains = null, + int|null $minContains = null, + int|null $maxContains = null, + array|null $prefixItems = null, array|null $patternProperties = null, array|null $unevaluatedProperties = null, + mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, + array|null $dependentRequired = null, + array|null $dependentSchemas = null, mixed $propertyNames = Undefined::UNDEFINED, mixed $const = Undefined::UNDEFINED, + mixed $if = Undefined::UNDEFINED, + mixed $then = Undefined::UNDEFINED, + mixed $else = Undefined::UNDEFINED, + mixed $contentSchema = Undefined::UNDEFINED, // abstract annotation ?array $x = null, @@ -123,11 +133,21 @@ public function __construct( 'additionalProperties' => $additionalProperties ?? Undefined::UNDEFINED, 'additionalItems' => $additionalItems ?? Undefined::UNDEFINED, 'contains' => $contains ?? Undefined::UNDEFINED, + 'minContains' => $minContains ?? Undefined::UNDEFINED, + 'maxContains' => $maxContains ?? Undefined::UNDEFINED, + 'prefixItems' => $prefixItems ?? Undefined::UNDEFINED, 'patternProperties' => $patternProperties ?? Undefined::UNDEFINED, 'unevaluatedProperties' => $unevaluatedProperties ?? Undefined::UNDEFINED, + 'unevaluatedItems' => $unevaluatedItems, 'dependencies' => $dependencies, + 'dependentRequired' => $dependentRequired ?? Undefined::UNDEFINED, + 'dependentSchemas' => $dependentSchemas ?? Undefined::UNDEFINED, 'propertyNames' => $propertyNames, 'const' => $const, + 'if' => $if, + 'then' => $then, + 'else' => $else, + 'contentSchema' => $contentSchema, // abstract annotation 'x' => $x ?? Undefined::UNDEFINED, diff --git a/src/Attributes/JsonContent.php b/src/Attributes/JsonContent.php index 0eb0a53ea..bfb88b020 100644 --- a/src/Attributes/JsonContent.php +++ b/src/Attributes/JsonContent.php @@ -95,11 +95,21 @@ public function __construct( bool|AdditionalProperties|null $additionalProperties = null, array|null $additionalItems = null, array|null $contains = null, + int|null $minContains = null, + int|null $maxContains = null, + array|null $prefixItems = null, array|null $patternProperties = null, array|null $unevaluatedProperties = null, + mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, + array|null $dependentRequired = null, + array|null $dependentSchemas = null, mixed $propertyNames = Undefined::UNDEFINED, mixed $const = Undefined::UNDEFINED, + mixed $if = Undefined::UNDEFINED, + mixed $then = Undefined::UNDEFINED, + mixed $else = Undefined::UNDEFINED, + mixed $contentSchema = Undefined::UNDEFINED, // abstract annotation ?array $x = null, @@ -147,11 +157,21 @@ public function __construct( 'additionalProperties' => $additionalProperties ?? Undefined::UNDEFINED, 'additionalItems' => $additionalItems ?? Undefined::UNDEFINED, 'contains' => $contains ?? Undefined::UNDEFINED, + 'minContains' => $minContains ?? Undefined::UNDEFINED, + 'maxContains' => $maxContains ?? Undefined::UNDEFINED, + 'prefixItems' => $prefixItems ?? Undefined::UNDEFINED, 'patternProperties' => $patternProperties ?? Undefined::UNDEFINED, 'unevaluatedProperties' => $unevaluatedProperties ?? Undefined::UNDEFINED, + 'unevaluatedItems' => $unevaluatedItems, 'dependencies' => $dependencies, + 'dependentRequired' => $dependentRequired ?? Undefined::UNDEFINED, + 'dependentSchemas' => $dependentSchemas ?? Undefined::UNDEFINED, 'propertyNames' => $propertyNames, 'const' => $const, + 'if' => $if, + 'then' => $then, + 'else' => $else, + 'contentSchema' => $contentSchema, // abstract annotation 'x' => $x ?? Undefined::UNDEFINED, diff --git a/src/Attributes/Property.php b/src/Attributes/Property.php index 5cd6a1031..08597b122 100644 --- a/src/Attributes/Property.php +++ b/src/Attributes/Property.php @@ -74,11 +74,21 @@ public function __construct( bool|AdditionalProperties|null $additionalProperties = null, array|null $additionalItems = null, array|null $contains = null, + int|null $minContains = null, + int|null $maxContains = null, + array|null $prefixItems = null, array|null $patternProperties = null, array|null $unevaluatedProperties = null, + mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, + array|null $dependentRequired = null, + array|null $dependentSchemas = null, mixed $propertyNames = Undefined::UNDEFINED, mixed $const = Undefined::UNDEFINED, + mixed $if = Undefined::UNDEFINED, + mixed $then = Undefined::UNDEFINED, + mixed $else = Undefined::UNDEFINED, + mixed $contentSchema = Undefined::UNDEFINED, // abstract annotation ?array $x = null, @@ -128,11 +138,21 @@ public function __construct( 'additionalProperties' => $additionalProperties ?? Undefined::UNDEFINED, 'additionalItems' => $additionalItems ?? Undefined::UNDEFINED, 'contains' => $contains ?? Undefined::UNDEFINED, + 'minContains' => $minContains ?? Undefined::UNDEFINED, + 'maxContains' => $maxContains ?? Undefined::UNDEFINED, + 'prefixItems' => $prefixItems ?? Undefined::UNDEFINED, 'patternProperties' => $patternProperties ?? Undefined::UNDEFINED, 'unevaluatedProperties' => $unevaluatedProperties ?? Undefined::UNDEFINED, + 'unevaluatedItems' => $unevaluatedItems, 'dependencies' => $dependencies, + 'dependentRequired' => $dependentRequired ?? Undefined::UNDEFINED, + 'dependentSchemas' => $dependentSchemas ?? Undefined::UNDEFINED, 'propertyNames' => $propertyNames, 'const' => $const, + 'if' => $if, + 'then' => $then, + 'else' => $else, + 'contentSchema' => $contentSchema, // abstract annotation 'x' => $x ?? Undefined::UNDEFINED, diff --git a/src/Attributes/Schema.php b/src/Attributes/Schema.php index 4e67596c4..2d5b02bbe 100644 --- a/src/Attributes/Schema.php +++ b/src/Attributes/Schema.php @@ -71,11 +71,21 @@ public function __construct( bool|AdditionalProperties|null $additionalProperties = null, array|null $additionalItems = null, array|null $contains = null, + int|null $minContains = null, + int|null $maxContains = null, + array|null $prefixItems = null, array|null $patternProperties = null, array|null $unevaluatedProperties = null, + mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, + array|null $dependentRequired = null, + array|null $dependentSchemas = null, mixed $propertyNames = Undefined::UNDEFINED, mixed $const = Undefined::UNDEFINED, + mixed $if = Undefined::UNDEFINED, + mixed $then = Undefined::UNDEFINED, + mixed $else = Undefined::UNDEFINED, + mixed $contentSchema = Undefined::UNDEFINED, // abstract annotation ?array $x = null, @@ -122,11 +132,21 @@ public function __construct( 'additionalProperties' => $additionalProperties ?? Undefined::UNDEFINED, 'additionalItems' => $additionalItems ?? Undefined::UNDEFINED, 'contains' => $contains ?? Undefined::UNDEFINED, + 'minContains' => $minContains ?? Undefined::UNDEFINED, + 'maxContains' => $maxContains ?? Undefined::UNDEFINED, + 'prefixItems' => $prefixItems ?? Undefined::UNDEFINED, 'patternProperties' => $patternProperties ?? Undefined::UNDEFINED, 'unevaluatedProperties' => $unevaluatedProperties ?? Undefined::UNDEFINED, + 'unevaluatedItems' => $unevaluatedItems, 'dependencies' => $dependencies, + 'dependentRequired' => $dependentRequired ?? Undefined::UNDEFINED, + 'dependentSchemas' => $dependentSchemas ?? Undefined::UNDEFINED, 'propertyNames' => $propertyNames, 'const' => $const, + 'if' => $if, + 'then' => $then, + 'else' => $else, + 'contentSchema' => $contentSchema, // abstract annotation 'x' => $x ?? Undefined::UNDEFINED, diff --git a/src/Attributes/XmlContent.php b/src/Attributes/XmlContent.php index 0ac828fde..1601973e6 100644 --- a/src/Attributes/XmlContent.php +++ b/src/Attributes/XmlContent.php @@ -74,11 +74,21 @@ public function __construct( bool|AdditionalProperties|null $additionalProperties = null, array|null $additionalItems = null, array|null $contains = null, + int|null $minContains = null, + int|null $maxContains = null, + array|null $prefixItems = null, array|null $patternProperties = null, array|null $unevaluatedProperties = null, + mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, + array|null $dependentRequired = null, + array|null $dependentSchemas = null, mixed $propertyNames = Undefined::UNDEFINED, mixed $const = Undefined::UNDEFINED, + mixed $if = Undefined::UNDEFINED, + mixed $then = Undefined::UNDEFINED, + mixed $else = Undefined::UNDEFINED, + mixed $contentSchema = Undefined::UNDEFINED, // abstract annotation ?array $x = null, @@ -126,11 +136,21 @@ public function __construct( 'additionalProperties' => $additionalProperties ?? Undefined::UNDEFINED, 'additionalItems' => $additionalItems ?? Undefined::UNDEFINED, 'contains' => $contains ?? Undefined::UNDEFINED, + 'minContains' => $minContains ?? Undefined::UNDEFINED, + 'maxContains' => $maxContains ?? Undefined::UNDEFINED, + 'prefixItems' => $prefixItems ?? Undefined::UNDEFINED, 'patternProperties' => $patternProperties ?? Undefined::UNDEFINED, 'unevaluatedProperties' => $unevaluatedProperties ?? Undefined::UNDEFINED, + 'unevaluatedItems' => $unevaluatedItems, 'dependencies' => $dependencies, + 'dependentRequired' => $dependentRequired ?? Undefined::UNDEFINED, + 'dependentSchemas' => $dependentSchemas ?? Undefined::UNDEFINED, 'propertyNames' => $propertyNames, 'const' => $const, + 'if' => $if, + 'then' => $then, + 'else' => $else, + 'contentSchema' => $contentSchema, // abstract annotation 'x' => $x ?? Undefined::UNDEFINED, diff --git a/src/Compiler/OpenApi31Compiler.php b/src/Compiler/OpenApi31Compiler.php index 15c8ddc9e..268461505 100644 --- a/src/Compiler/OpenApi31Compiler.php +++ b/src/Compiler/OpenApi31Compiler.php @@ -546,6 +546,7 @@ protected function compileSchema(OA\Schema|string $schema): array|\stdClass 'pattern' => $schema->pattern, 'contentMediaType' => $schema->contentMediaType, 'contentEncoding' => $schema->contentEncoding, + 'contentSchema' => $schema->contentSchema instanceof OA\Schema ? $this->compileSchema($schema->contentSchema) : null, // Numeric 'minimum' => $this->compileMinimum($schema), @@ -873,7 +874,7 @@ protected function validateSchemas(Specification $specification): void foreach ($allSchemas as $schema) { if ($schema->type !== null && (is_array($schema->type) ? in_array('array', $schema->type, true) : $schema->type === 'array')) { - if ($schema->items === null) { + if ($schema->items === null && $schema->prefixItems === null && $schema->contains === null) { $this->logger->warning('Schema' . ($schema->schema ? " \"$schema->schema\"" : '') . ' has type "array" but no items in ' . $schema->getSourceLocation()); } } diff --git a/src/HybridBridge.php b/src/HybridBridge.php index 4a19ef417..ffcec8a50 100644 --- a/src/HybridBridge.php +++ b/src/HybridBridge.php @@ -661,6 +661,7 @@ protected function convertSchema(OA\Schema $schema): Spec\Schema pattern: $this->val($schema->pattern), contentMediaType: $this->val($schema->contentMediaType), contentEncoding: $this->val($schema->contentEncoding), + contentSchema: Undefined::isDefault($schema->contentSchema) ? null : $this->convertSchemaValue($schema->contentSchema), minimum: $this->val($schema->minimum), maximum: $this->val($schema->maximum), exclusiveMinimum: $this->val($schema->exclusiveMinimum), @@ -670,7 +671,11 @@ protected function convertSchema(OA\Schema $schema): Spec\Schema minItems: $this->val($schema->minItems), maxItems: $this->val($schema->maxItems), uniqueItems: $this->val($schema->uniqueItems), + prefixItems: Undefined::isDefault($schema->prefixItems) ? null : array_map($this->convertSchemaValue(...), $schema->prefixItems), contains: $this->convertSchemaOrBool($schema->contains), + minContains: $this->val($schema->minContains), + maxContains: $this->val($schema->maxContains), + unevaluatedItems: $this->convertSchemaOrBool($schema->unevaluatedItems), properties: $properties, required: Undefined::isDefault($schema->required) ? null : $schema->required, additionalProperties: $this->convertAdditionalProperties($schema), @@ -683,10 +688,15 @@ protected function convertSchema(OA\Schema $schema): Spec\Schema propertyNames: Undefined::isDefault($schema->propertyNames) ? null : $this->convertSchema($schema->propertyNames), + dependentRequired: Undefined::isDefault($schema->dependentRequired) ? null : $schema->dependentRequired, + dependentSchemas: Undefined::isDefault($schema->dependentSchemas) ? null : array_map($this->convertSchemaValue(...), $schema->dependentSchemas), allOf: Undefined::isDefault($schema->allOf) ? null : array_map($this->convertSchema(...), $schema->allOf), anyOf: Undefined::isDefault($schema->anyOf) ? null : array_map($this->convertSchema(...), $schema->anyOf), oneOf: Undefined::isDefault($schema->oneOf) ? null : array_map($this->convertSchema(...), $schema->oneOf), not: Undefined::isDefault($schema->not) ? null : $this->convertSchema($schema->not), + if: Undefined::isDefault($schema->if) ? null : $this->convertSchemaValue($schema->if), + then: Undefined::isDefault($schema->then) ? null : $this->convertSchemaValue($schema->then), + else: Undefined::isDefault($schema->else) ? null : $this->convertSchemaValue($schema->else), enum: Undefined::isDefault($schema->enum) ? null : $schema->enum, const: Undefined::isDefault($schema->const) ? Undefined::UNDEFINED : $schema->const, example: Undefined::isDefault($schema->example) ? Undefined::UNDEFINED : $schema->example, @@ -718,11 +728,26 @@ protected function convertSchemaOrBool(mixed $value): Spec\Schema|bool|null { return match (true) { Undefined::isDefault($value) => null, - $value instanceof OA\Schema => $this->convertSchema($value), + $value instanceof OA\Schema, is_array($value) => $this->convertSchemaValue($value), default => $value, }; } + /** + * Convert a schema-valued keyword; classic docblocks and attributes may carry the + * schema as a plain array instead of a Schema annotation. + * + * @param OA\Schema|array $value + */ + protected function convertSchemaValue(OA\Schema|array $value): Spec\Schema + { + if (is_array($value)) { + $value = new OA\Schema([...$value, '_context' => new Context(['generated' => true])]); + } + + return $this->convertSchema($value); + } + protected function convertProperty(OA\Property $prop): Spec\Property { $schema = $this->convertSchema($prop); diff --git a/src/Spec/Schema.php b/src/Spec/Schema.php index c8ebdd301..219ce73ab 100644 --- a/src/Spec/Schema.php +++ b/src/Spec/Schema.php @@ -68,6 +68,7 @@ class Schema extends AbstractAttribute * @param string|null $pattern Regular expression pattern the string must match * @param string|null $contentMediaType The media type of string content encoding * @param string|null $contentEncoding The encoding used for string content (e.g. base64) + * @param Schema|null $contentSchema Schema for the decoded content described by contentMediaType * @param int|float|null $minimum Minimum numeric value (inclusive) * @param int|float|null $maximum Maximum numeric value (inclusive) * @param int|float|bool|null $exclusiveMinimum Exclusive minimum value @@ -133,6 +134,7 @@ public function __construct( public ?string $pattern = null, public ?string $contentMediaType = null, public ?string $contentEncoding = null, + public ?Schema $contentSchema = null, // Numeric constraints public int|float|null $minimum = null, diff --git a/tests/Fixtures/Scratch/SchemaKeywords-spec.php b/tests/Fixtures/Scratch/SchemaKeywords-spec.php new file mode 100644 index 000000000..78e654134 --- /dev/null +++ b/tests/Fixtures/Scratch/SchemaKeywords-spec.php @@ -0,0 +1,97 @@ + ['billingAddress']], + dependentSchemas: ['creditCard' => new OA\Schema(required: ['billingAddress'])], +)] +class SchemaKeywordsDependentSpec +{ +} + +#[OA\Schema( + schema: 'embedded', + type: 'string', + contentEncoding: 'base64', + contentMediaType: 'application/json', + contentSchema: new OA\Schema(type: 'object'), +)] +class SchemaKeywordsEmbeddedSpec +{ +} + +#[OA\Schema(schema: 'decoded', type: 'object')] +class SchemaKeywordsDecodedSpec +{ +} + +// a nested schema slot takes a ref like any other schema position +#[OA\Schema( + schema: 'envelope', + type: 'string', + contentMediaType: 'application/json', + contentSchema: new OA\Schema\Ref(SchemaKeywordsDecodedSpec::class), +)] +class SchemaKeywordsEnvelopeSpec +{ +} + +#[OA\Info(title: 'SchemaKeywords', version: '1.0')] +class SchemaKeywordsControllerSpec +{ + #[OA\Operation\Get(path: '/endpoint', operationId: 'endpoint')] + #[OA\Response( + response: 200, + description: 'OK', + content: new OA\MediaType\Json(ref: '#/components/schemas/conditional'), + )] + public function endpoint(): void + { + } +} diff --git a/tests/Fixtures/Scratch/SchemaKeywords.php b/tests/Fixtures/Scratch/SchemaKeywords.php new file mode 100644 index 000000000..efaa81465 --- /dev/null +++ b/tests/Fixtures/Scratch/SchemaKeywords.php @@ -0,0 +1,102 @@ + 'string'], + minContains: 1, + maxContains: 3, + unevaluatedItems: false, + unevaluatedProperties: ['type' => 'string'], +)] +class SchemaKeywordsTuple +{ +} + +#[OAT\Schema( + schema: 'dependent', + type: 'object', + dependentRequired: ['creditCard' => ['billingAddress']], + dependentSchemas: ['creditCard' => new OAT\Schema(required: ['billingAddress'])], +)] +class SchemaKeywordsDependent +{ +} + +#[OAT\Schema( + schema: 'embedded', + type: 'string', + contentEncoding: 'base64', + contentMediaType: 'application/json', + contentSchema: new OAT\Schema(type: 'object'), +)] +class SchemaKeywordsEmbedded +{ +} + +#[OAT\Schema(schema: 'decoded', type: 'object')] +class SchemaKeywordsDecoded +{ +} + +// a nested schema slot takes a ref like any other schema position +#[OAT\Schema( + schema: 'envelope', + type: 'string', + contentMediaType: 'application/json', + contentSchema: new OAT\Schema(ref: SchemaKeywordsDecoded::class), +)] +class SchemaKeywordsEnvelope +{ +} + +#[OAT\Info(title: 'SchemaKeywords', version: '1.0')] +class SchemaKeywordsController +{ + #[OAT\Get( + path: '/endpoint', + operationId: 'endpoint', + responses: [ + new OAT\Response( + response: 200, + description: 'OK', + content: new OAT\JsonContent(ref: '#/components/schemas/conditional'), + ), + ], + )] + public function endpoint(): void + { + } +} diff --git a/tests/Fixtures/Scratch/SchemaKeywords3.0.0.yaml b/tests/Fixtures/Scratch/SchemaKeywords3.0.0.yaml new file mode 100644 index 000000000..ae923f2c2 --- /dev/null +++ b/tests/Fixtures/Scratch/SchemaKeywords3.0.0.yaml @@ -0,0 +1,36 @@ +openapi: 3.0.0 +info: + title: SchemaKeywords + version: '1.0' +paths: + /endpoint: + get: + operationId: endpoint + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/conditional' +components: + schemas: + conditional: + type: object + properties: + country: + type: string + postalCode: + type: string + region: + type: string + tuple: + type: array + dependent: + type: object + embedded: + type: string + decoded: + type: object + envelope: + type: string diff --git a/tests/Fixtures/Scratch/SchemaKeywords3.1.0.yaml b/tests/Fixtures/Scratch/SchemaKeywords3.1.0.yaml new file mode 100644 index 000000000..498b3b07e --- /dev/null +++ b/tests/Fixtures/Scratch/SchemaKeywords3.1.0.yaml @@ -0,0 +1,71 @@ +openapi: 3.1.0 +info: + title: SchemaKeywords + version: '1.0' +paths: + /endpoint: + get: + operationId: endpoint + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/conditional' +components: + schemas: + conditional: + type: object + properties: + country: + type: string + postalCode: + type: string + region: + type: string + if: + required: + - country + then: + required: + - postalCode + else: + required: + - region + tuple: + type: array + prefixItems: + - + type: string + - + type: integer + contains: + type: string + minContains: 1 + maxContains: 3 + unevaluatedItems: false + unevaluatedProperties: + type: string + dependent: + type: object + dependentRequired: + creditCard: + - billingAddress + dependentSchemas: + creditCard: + required: + - billingAddress + embedded: + type: string + contentMediaType: application/json + contentEncoding: base64 + contentSchema: + type: object + decoded: + type: object + envelope: + type: string + contentMediaType: application/json + contentSchema: + $ref: '#/components/schemas/decoded' diff --git a/tests/Fixtures/Scratch/SchemaKeywords3.2.0.yaml b/tests/Fixtures/Scratch/SchemaKeywords3.2.0.yaml new file mode 100644 index 000000000..1475eeeb1 --- /dev/null +++ b/tests/Fixtures/Scratch/SchemaKeywords3.2.0.yaml @@ -0,0 +1,71 @@ +openapi: 3.2.0 +info: + title: SchemaKeywords + version: '1.0' +paths: + /endpoint: + get: + operationId: endpoint + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/conditional' +components: + schemas: + conditional: + type: object + properties: + country: + type: string + postalCode: + type: string + region: + type: string + if: + required: + - country + then: + required: + - postalCode + else: + required: + - region + tuple: + type: array + prefixItems: + - + type: string + - + type: integer + contains: + type: string + minContains: 1 + maxContains: 3 + unevaluatedItems: false + unevaluatedProperties: + type: string + dependent: + type: object + dependentRequired: + creditCard: + - billingAddress + dependentSchemas: + creditCard: + required: + - billingAddress + embedded: + type: string + contentMediaType: application/json + contentEncoding: base64 + contentSchema: + type: object + decoded: + type: object + envelope: + type: string + contentMediaType: application/json + contentSchema: + $ref: '#/components/schemas/decoded' diff --git a/tests/ScratchTest.php b/tests/ScratchTest.php index bd506e095..b57aae1ed 100644 --- a/tests/ScratchTest.php +++ b/tests/ScratchTest.php @@ -28,6 +28,15 @@ public static function scratchTestCases(): iterable // for a diagnostic only one mode raises. Both keys contribute when both are present. $expectedLogs = [ 'Auth-3.0.0' => ['mutualTLS security schemes are not supported in OpenAPI 3.0 and will be omitted'], + 'SchemaKeywords-3.0.0' => [ + 'prefixItems is not supported in OpenAPI 3.0', + 'unevaluatedProperties is not supported in OpenAPI 3.0', + 'unevaluatedItems is not supported in OpenAPI 3.0', + 'if/then/else is not supported in OpenAPI 3.0', + ], + 'SchemaKeywords-3.0.0-classic' => ['@OA\Items() is required when @OA\Schema() has type "array"'], + 'SchemaKeywords-3.0.0-spec' => ['has type "array" but no items'], + 'SchemaKeywords-3.0.0-hybrid' => ['has type "array" but no items'], 'Examples-3.0.0-classic' => ['@OA\Schema::examples is only allowed as of 3.1.0'], 'Examples-3.0.0-spec' => ['examples array is not supported in OpenAPI 3.0'], 'Examples-3.0.0-hybrid' => ['examples array is not supported in OpenAPI 3.0'],