Skip to content

Commit

Permalink
Add validation rule that example/examples are mutually exclusive and …
Browse files Browse the repository at this point in the history
…add examples property to Schema (3.1.0 only) (zircote#1561)
  • Loading branch information
DerManoMann authored Mar 23, 2024
1 parent d3265da commit 2357faf
Show file tree
Hide file tree
Showing 87 changed files with 1,084 additions and 166 deletions.
11 changes: 9 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
34 changes: 31 additions & 3 deletions src/Annotations/AbstractAnnotation.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ public function __set(string $property, $value): void
$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.
*
Expand Down Expand Up @@ -350,7 +360,7 @@ public function jsonSerialize()
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})) {
Expand All @@ -361,7 +371,7 @@ public function jsonSerialize()
}
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;
Expand All @@ -381,7 +391,18 @@ public function jsonSerialize()
$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)) {
Expand Down Expand Up @@ -540,6 +561,13 @@ public function validate(array $stack = [], array $skip = [], string $ref = '',
}
$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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Annotations/Components.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class Components extends AbstractAnnotation
/**
* Reusable Examples.
*
* @var Examples[]
* @var array<Examples>
*/
public $examples = Generator::UNDEFINED;

Expand Down
1 change: 1 addition & 0 deletions src/Annotations/Examples.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class Examples extends AbstractAnnotation

public static $_parents = [
Components::class,
Schema::class,
Parameter::class,
PathParameter::class,
MediaType::class,
Expand Down
2 changes: 2 additions & 0 deletions src/Annotations/HeaderParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

namespace OpenApi\Annotations;

use OpenApi\Annotations as OA;

/**
* A `@OA\Request` header parameter.
*
Expand Down
12 changes: 1 addition & 11 deletions src/Annotations/JsonContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace OpenApi\Annotations;

use OpenApi\Generator;
use OpenApi\Annotations as OA;

/**
* Shorthand for a json response.
Expand All @@ -17,16 +17,6 @@
*/
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
*/
Expand Down
4 changes: 2 additions & 2 deletions src/Annotations/License.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public function jsonSerialize()
{
$data = parent::jsonSerialize();

if ($this->_context->isVersion(OpenApi::VERSION_3_0_0)) {
if ($this->isOpenApiVersion(OpenApi::VERSION_3_0_0)) {
unset($data->identifier);
}

Expand All @@ -90,7 +90,7 @@ public function validate(array $stack = [], array $skip = [], string $ref = '',
{
$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;
Expand Down
8 changes: 3 additions & 5 deletions src/Annotations/MediaType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/Annotations/OpenApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ public function jsonSerialize()
{
$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);
}

Expand Down
1 change: 1 addition & 0 deletions src/Annotations/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/Annotations/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 2 additions & 0 deletions src/Annotations/PathParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

namespace OpenApi\Annotations;

use OpenApi\Annotations as OA;

/**
* A `@OA\Request` path parameter.
*
Expand Down
2 changes: 2 additions & 0 deletions src/Annotations/QueryParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

namespace OpenApi\Annotations;

use OpenApi\Annotations as OA;

/**
* A `@OA\Request` query parameter.
*
Expand Down
54 changes: 42 additions & 12 deletions src/Annotations/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,17 @@ 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
*/
public $maxProperties = Generator::UNDEFINED;

/**
* 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
*/
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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)
*
Expand All @@ -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)
*
Expand All @@ -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)
*
Expand All @@ -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)
*
Expand All @@ -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)
*
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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'],
Expand All @@ -463,8 +484,9 @@ public function jsonSerialize()
{
$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);
}
Expand All @@ -484,6 +506,14 @@ public function validate(array $stack = [], array $skip = [], string $ref = '',
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);
}
}
Loading

0 comments on commit 2357faf

Please sign in to comment.