From 9b40e03c801994f717f64a6f734f8f3733d39dde Mon Sep 17 00:00:00 2001 From: Quentin Dequippe Date: Tue, 1 Aug 2023 14:00:24 +0400 Subject: [PATCH 1/7] feat: webhooks --- docs/reference/annotations.md | 2 ++ docs/reference/attributes.md | 2 ++ src/Annotations/OpenApi.php | 7 +++++++ src/Attributes/OpenApi.php | 16 +++++++++------- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/docs/reference/annotations.md b/docs/reference/annotations.md index 2208ec7af..c98bd0e24 100644 --- a/docs/reference/annotations.md +++ b/docs/reference/annotations.md @@ -566,6 +566,8 @@ The list of values includes alternative security requirement objects that can be Only one of the security requirement objects need to be satisfied to authorize a request.
Individual operations can override this definition.
To make security optional, an empty security requirement `({})` can be included in the array.

+
webhooks : array<string,PathItem>
+

The available webhooks for the API.

#### Reference diff --git a/docs/reference/attributes.md b/docs/reference/attributes.md index c4ebcf0c2..52f6cdae6 100644 --- a/docs/reference/attributes.md +++ b/docs/reference/attributes.md @@ -1466,6 +1466,8 @@ The keys inside the array will be prefixed with `x-`.

attachables : Attachable[]|null

Arbitrary attachables for this annotation.
These will be ignored but can be used for custom processing.

+
webhooks : array<string,PathItem>|null
+

The available webhooks for the API.

## [Options](https://github.com/zircote/swagger-php/tree/master/src/Attributes/Options.php) diff --git a/src/Annotations/OpenApi.php b/src/Annotations/OpenApi.php index 74775276b..6c593d110 100644 --- a/src/Annotations/OpenApi.php +++ b/src/Annotations/OpenApi.php @@ -98,6 +98,13 @@ class OpenApi extends AbstractAnnotation */ public $externalDocs = Generator::UNDEFINED; + /** + * The available webhooks for the API. + * + * @var array + */ + public $webhooks = Generator::UNDEFINED; + /** * @var Analysis */ diff --git a/src/Attributes/OpenApi.php b/src/Attributes/OpenApi.php index ce625e21c..0bac57df4 100644 --- a/src/Attributes/OpenApi.php +++ b/src/Attributes/OpenApi.php @@ -12,11 +12,12 @@ class OpenApi extends \OpenApi\Annotations\OpenApi { /** - * @param Server[]|null $servers - * @param Tag[]|null $tags - * @param PathItem[]|null $paths - * @param array|null $x - * @param Attachable[]|null $attachables + * @param Server[]|null $servers + * @param Tag[]|null $tags + * @param PathItem[]|null $paths + * @param array|null $x + * @param Attachable[]|null $attachables + * @param array|null $webhooks */ public function __construct( string $openapi = self::DEFAULT_VERSION, @@ -29,13 +30,14 @@ public function __construct( ?Components $components = null, // annotation ?array $x = null, - ?array $attachables = null + ?array $attachables = null, + ?array $webhooks = null, ) { parent::__construct([ 'openapi' => $openapi, 'security' => $security ?? Generator::UNDEFINED, 'x' => $x ?? Generator::UNDEFINED, - 'value' => $this->combine($info, $servers, $tags, $externalDocs, $paths, $components, $attachables), + 'value' => $this->combine($info, $servers, $tags, $externalDocs, $paths, $components, $attachables, $webhooks), ]); } } From f913b6952e018eb3244049f200d1e72dba5352e2 Mon Sep 17 00:00:00 2001 From: Quentin Dequippe Date: Tue, 1 Aug 2023 14:07:00 +0400 Subject: [PATCH 2/7] add more value type for reference object --- src/Annotations/OpenApi.php | 2 +- src/Attributes/OpenApi.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Annotations/OpenApi.php b/src/Annotations/OpenApi.php index 6c593d110..843197492 100644 --- a/src/Annotations/OpenApi.php +++ b/src/Annotations/OpenApi.php @@ -101,7 +101,7 @@ class OpenApi extends AbstractAnnotation /** * The available webhooks for the API. * - * @var array + * @var array Array of PathItem object or Reference object */ public $webhooks = Generator::UNDEFINED; diff --git a/src/Attributes/OpenApi.php b/src/Attributes/OpenApi.php index 0bac57df4..aabbefc37 100644 --- a/src/Attributes/OpenApi.php +++ b/src/Attributes/OpenApi.php @@ -17,7 +17,7 @@ class OpenApi extends \OpenApi\Annotations\OpenApi * @param PathItem[]|null $paths * @param array|null $x * @param Attachable[]|null $attachables - * @param array|null $webhooks + * @param array|null $webhooks */ public function __construct( string $openapi = self::DEFAULT_VERSION, From 68395992aeeb702ab9a20e6473f84bcea60c994c Mon Sep 17 00:00:00 2001 From: Quentin Dequippe Date: Tue, 1 Aug 2023 16:33:12 +0400 Subject: [PATCH 3/7] remove phpdoc comment --- docs/reference/annotations.md | 2 +- docs/reference/attributes.md | 2 +- src/Annotations/OpenApi.php | 2 +- src/Attributes/OpenApi.php | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/reference/annotations.md b/docs/reference/annotations.md index c98bd0e24..3211ba896 100644 --- a/docs/reference/annotations.md +++ b/docs/reference/annotations.md @@ -566,7 +566,7 @@ The list of values includes alternative security requirement objects that can be Only one of the security requirement objects need to be satisfied to authorize a request.
Individual operations can override this definition.
To make security optional, an empty security requirement `({})` can be included in the array.

-
webhooks : array<string,PathItem>
+
webhooks : array<string,PathItem|string|class-string|object>

The available webhooks for the API.

diff --git a/docs/reference/attributes.md b/docs/reference/attributes.md index 52f6cdae6..ac23680a8 100644 --- a/docs/reference/attributes.md +++ b/docs/reference/attributes.md @@ -1466,7 +1466,7 @@ The keys inside the array will be prefixed with `x-`.

attachables : Attachable[]|null

Arbitrary attachables for this annotation.
These will be ignored but can be used for custom processing.

-
webhooks : array<string,PathItem>|null
+
webhooks : array<string,PathItem|string|class-string|object>|null

The available webhooks for the API.

diff --git a/src/Annotations/OpenApi.php b/src/Annotations/OpenApi.php index 843197492..b377d164b 100644 --- a/src/Annotations/OpenApi.php +++ b/src/Annotations/OpenApi.php @@ -101,7 +101,7 @@ class OpenApi extends AbstractAnnotation /** * The available webhooks for the API. * - * @var array Array of PathItem object or Reference object + * @var array */ public $webhooks = Generator::UNDEFINED; diff --git a/src/Attributes/OpenApi.php b/src/Attributes/OpenApi.php index aabbefc37..855b69566 100644 --- a/src/Attributes/OpenApi.php +++ b/src/Attributes/OpenApi.php @@ -12,12 +12,12 @@ class OpenApi extends \OpenApi\Annotations\OpenApi { /** - * @param Server[]|null $servers - * @param Tag[]|null $tags - * @param PathItem[]|null $paths - * @param array|null $x - * @param Attachable[]|null $attachables - * @param array|null $webhooks + * @param Server[]|null $servers + * @param Tag[]|null $tags + * @param PathItem[]|null $paths + * @param array|null $x + * @param Attachable[]|null $attachables + * @param array|null $webhooks */ public function __construct( string $openapi = self::DEFAULT_VERSION, From 54e130d932998c6972311f3eab6c125b30011c2d Mon Sep 17 00:00:00 2001 From: Quentin Dequippe Date: Wed, 2 Aug 2023 16:06:53 +0400 Subject: [PATCH 4/7] add test --- Examples/webhook-example/OpenApiSpec.php | 36 +++++++++++++++++++ Examples/webhook-example/Pet.php | 34 ++++++++++++++++++ Examples/webhook-example/webhook-example.yaml | 30 ++++++++++++++++ src/Annotations/OpenApi.php | 4 +-- src/Attributes/OpenApi.php | 17 ++++----- tests/ExamplesTest.php | 8 +++++ 6 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 Examples/webhook-example/OpenApiSpec.php create mode 100644 Examples/webhook-example/Pet.php create mode 100644 Examples/webhook-example/webhook-example.yaml diff --git a/Examples/webhook-example/OpenApiSpec.php b/Examples/webhook-example/OpenApiSpec.php new file mode 100644 index 000000000..854722533 --- /dev/null +++ b/Examples/webhook-example/OpenApiSpec.php @@ -0,0 +1,36 @@ + + * @var array */ public $webhooks = Generator::UNDEFINED; @@ -113,7 +113,7 @@ class OpenApi extends AbstractAnnotation /** * @inheritdoc */ - public static $_required = ['openapi', 'info', 'paths']; + public static $_required = ['openapi', 'info']; /** * @inheritdoc diff --git a/src/Attributes/OpenApi.php b/src/Attributes/OpenApi.php index 855b69566..874578a15 100644 --- a/src/Attributes/OpenApi.php +++ b/src/Attributes/OpenApi.php @@ -6,18 +6,19 @@ namespace OpenApi\Attributes; +use OpenApi\Annotations\PathItem; use OpenApi\Generator; #[\Attribute(\Attribute::TARGET_CLASS)] class OpenApi extends \OpenApi\Annotations\OpenApi { /** - * @param Server[]|null $servers - * @param Tag[]|null $tags - * @param PathItem[]|null $paths - * @param array|null $x - * @param Attachable[]|null $attachables - * @param array|null $webhooks + * @param Server[]|null $servers + * @param Tag[]|null $tags + * @param PathItem[]|null $paths + * @param array|null $x + * @param Attachable[]|null $attachables + * @param array|null $webhooks */ public function __construct( string $openapi = self::DEFAULT_VERSION, @@ -28,10 +29,10 @@ public function __construct( ?ExternalDocumentation $externalDocs = null, ?array $paths = null, ?Components $components = null, + ?array $webhooks = null, // annotation ?array $x = null, - ?array $attachables = null, - ?array $webhooks = null, + ?array $attachables = null ) { parent::__construct([ 'openapi' => $openapi, diff --git a/tests/ExamplesTest.php b/tests/ExamplesTest.php index 239ac243e..f92150978 100644 --- a/tests/ExamplesTest.php +++ b/tests/ExamplesTest.php @@ -140,6 +140,14 @@ public function exampleDetails(): iterable [], ]; + yield 'webhook-example' => [ + OA\OpenApi::VERSION_3_1_0, + 'webhook-example', + 'webhook-example.yaml', + false, + [], + ]; + if (\PHP_VERSION_ID >= 80100) { yield 'using-links-php81' => [ OA\OpenApi::VERSION_3_0_0, From 1640feb5a690f2e28707129d1e03d0e5a517cd5b Mon Sep 17 00:00:00 2001 From: Quentin Dequippe Date: Thu, 10 Aug 2023 18:21:21 +0400 Subject: [PATCH 5/7] add validation based on version --- Examples/webhook-example/OpenApiSpec.php | 26 +++++++++--------- Examples/webhook-example/webhook-example.yaml | 1 + src/Annotations/OpenApi.php | 27 +++++++++++++++++++ src/Attributes/OpenApi.php | 2 +- tests/Annotations/OpenApiTest.php | 14 +++++++--- tests/ContextTest.php | 2 +- tests/GeneratorTest.php | 3 +-- 7 files changed, 54 insertions(+), 21 deletions(-) diff --git a/Examples/webhook-example/OpenApiSpec.php b/Examples/webhook-example/OpenApiSpec.php index 854722533..5ce29887f 100644 --- a/Examples/webhook-example/OpenApiSpec.php +++ b/Examples/webhook-example/OpenApiSpec.php @@ -11,23 +11,21 @@ * title="Webhook Example", * ), * webhooks={ - * { - * "newPet": @OA\PathItem( - * @OA\Post( - * @OA\RequestBody( - * description="Information about a new pet in the system", - * @OA\MediaType( - * mediaType="application/xml", - * @OA\Schema(ref="#/components/schemas/Pet") - * ), + * "newPet": @OA\PathItem( + * @OA\Post( + * @OA\RequestBody( + * description="Information about a new pet in the system", + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(ref="#/components/schemas/Pet") * ), - * @OA\Response( - * response=200, - * description="Return a 200 status to indicate that the data was received successfully" - * ) + * ), + * @OA\Response( + * response=200, + * description="Return a 200 status to indicate that the data was received successfully" * ) * ) - * } + * ) * } * ) */ diff --git a/Examples/webhook-example/webhook-example.yaml b/Examples/webhook-example/webhook-example.yaml index d0f30193e..e7bf67184 100644 --- a/Examples/webhook-example/webhook-example.yaml +++ b/Examples/webhook-example/webhook-example.yaml @@ -16,6 +16,7 @@ components: type: string tag: type: string + type: object webhooks: newPet: post: diff --git a/src/Annotations/OpenApi.php b/src/Annotations/OpenApi.php index 32c4addd1..32cfdf48b 100644 --- a/src/Annotations/OpenApi.php +++ b/src/Annotations/OpenApi.php @@ -150,6 +150,18 @@ public function validate(array $stack = null, array $skip = null, string $ref = return false; } + if ($this->openapi === self::VERSION_3_0_0 && Generator::isDefault($this->paths)) { + $this->_context->logger->warning('The OpenAPI document must contain paths field'); + + return false; + } + + if ($this->openapi === self::VERSION_3_1_0 && Generator::isDefault($this->paths) && Generator::isDefault($this->webhooks) && Generator::isDefault($this->components)) { + $this->_context->logger->warning('The OpenAPI document must contain at least one paths field, a components field or a webhooks field'); + + return false; + } + return parent::validate([], [], '#', new \stdClass()); } @@ -237,4 +249,19 @@ private static function resolveRef(string $ref, string $resolved, $container, ar throw new \Exception('$ref "' . $unresolved . '" not found'); } + + /** + * @inheritdoc + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $data = parent::jsonSerialize(); + + if (false === $this->_context->isVersion(OpenApi::VERSION_3_1_0)) { + unset($data->webhooks); + } + + return $data; + } } diff --git a/src/Attributes/OpenApi.php b/src/Attributes/OpenApi.php index 874578a15..9ffce16e0 100644 --- a/src/Attributes/OpenApi.php +++ b/src/Attributes/OpenApi.php @@ -16,9 +16,9 @@ class OpenApi extends \OpenApi\Annotations\OpenApi * @param Server[]|null $servers * @param Tag[]|null $tags * @param PathItem[]|null $paths + * @param array|null $webhooks * @param array|null $x * @param Attachable[]|null $attachables - * @param array|null $webhooks */ public function __construct( string $openapi = self::DEFAULT_VERSION, diff --git a/tests/Annotations/OpenApiTest.php b/tests/Annotations/OpenApiTest.php index cf7862647..33e48a171 100644 --- a/tests/Annotations/OpenApiTest.php +++ b/tests/Annotations/OpenApiTest.php @@ -11,10 +11,18 @@ class OpenApiTest extends OpenApiTestCase { - public function testValidVersion(): void + public function testValidVersion310(): void { - $this->assertOpenApiLogEntryContains('Required @OA\Info() not found'); - $this->assertOpenApiLogEntryContains('Required @OA\PathItem() not found'); + $this->assertOpenApiLogEntryContains('The OpenAPI document must contain at least one paths field, a components field or a webhooks field'); + + $openapi = new OA\OpenApi(['_context' => $this->getContext()]); + $openapi->openapi = '3.1.0'; + $openapi->validate(); + } + + public function testValidVersion300(): void + { + $this->assertOpenApiLogEntryContains('The OpenAPI document must contain paths field'); $openapi = new OA\OpenApi(['_context' => $this->getContext()]); $openapi->openapi = '3.0.0'; diff --git a/tests/ContextTest.php b/tests/ContextTest.php index 4604b8679..d6e3cea89 100644 --- a/tests/ContextTest.php +++ b/tests/ContextTest.php @@ -26,7 +26,7 @@ public function testDetect(): void public function testFullyQualifiedName(): void { - $this->assertOpenApiLogEntryContains('Required @OA\PathItem() not found'); + $this->assertOpenApiLogEntryContains('The OpenAPI document must contain paths field'); $openapi = (new Generator($this->getTrackingLogger())) ->setAnalyser(new TokenAnalyser()) ->generate([$this->fixture('Customer.php')]); diff --git a/tests/GeneratorTest.php b/tests/GeneratorTest.php index 3dbd1c3f6..3853cbade 100644 --- a/tests/GeneratorTest.php +++ b/tests/GeneratorTest.php @@ -38,8 +38,7 @@ public function testScan(string $sourceDir, iterable $sources): void public function testScanInvalidSource(): void { $this->assertOpenApiLogEntryContains('Skipping invalid source: /tmp/__swagger_php_does_not_exist__'); - $this->assertOpenApiLogEntryContains('Required @OA\Info() not found'); - $this->assertOpenApiLogEntryContains('Required @OA\PathItem() not found'); + $this->assertOpenApiLogEntryContains('The OpenAPI document must contain paths field'); (new Generator($this->getTrackingLogger())) ->setAnalyser($this->getAnalyzer()) From 860be67205be58bd5750d7df97a056c408819174 Mon Sep 17 00:00:00 2001 From: Quentin Dequippe Date: Mon, 11 Sep 2023 09:51:32 +0400 Subject: [PATCH 6/7] Use custom annotation/attribute for Webhook, apply reviews --- src/Annotations/OpenApi.php | 6 +++--- src/Annotations/Webhook.php | 25 +++++++++++++++++++++++ src/Attributes/OpenApi.php | 13 ++++++------ src/Attributes/Webhook.php | 34 +++++++++++++++++++++++++++++++ src/Serializer.php | 1 + tests/Annotations/OpenApiTest.php | 4 ++-- tests/ContextTest.php | 2 +- tests/GeneratorTest.php | 2 +- 8 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 src/Annotations/Webhook.php create mode 100644 src/Attributes/Webhook.php diff --git a/src/Annotations/OpenApi.php b/src/Annotations/OpenApi.php index 32cfdf48b..0be0cd72c 100644 --- a/src/Annotations/OpenApi.php +++ b/src/Annotations/OpenApi.php @@ -101,7 +101,7 @@ class OpenApi extends AbstractAnnotation /** * The available webhooks for the API. * - * @var array + * @var Webhook[] */ public $webhooks = Generator::UNDEFINED; @@ -151,13 +151,13 @@ public function validate(array $stack = null, array $skip = null, string $ref = } if ($this->openapi === self::VERSION_3_0_0 && Generator::isDefault($this->paths)) { - $this->_context->logger->warning('The OpenAPI document must contain paths field'); + $this->_context->logger->warning('Required @OA\PathItem() not found'); return false; } if ($this->openapi === self::VERSION_3_1_0 && Generator::isDefault($this->paths) && Generator::isDefault($this->webhooks) && Generator::isDefault($this->components)) { - $this->_context->logger->warning('The OpenAPI document must contain at least one paths field, a components field or a webhooks field'); + $this->_context->logger->warning("At least one of 'Required @OA\Info(), @OA\Components() or @OA\Webhook() not found'"); return false; } diff --git a/src/Annotations/Webhook.php b/src/Annotations/Webhook.php new file mode 100644 index 000000000..4fb581f0f --- /dev/null +++ b/src/Annotations/Webhook.php @@ -0,0 +1,25 @@ +|null $webhooks - * @param array|null $x - * @param Attachable[]|null $attachables + * @param Server[]|null $servers + * @param Tag[]|null $tags + * @param PathItem[]|null $paths + * @param Webhook[]|null $webhooks + * @param array|null $x + * @param Attachable[]|null $attachables */ public function __construct( string $openapi = self::DEFAULT_VERSION, diff --git a/src/Attributes/Webhook.php b/src/Attributes/Webhook.php new file mode 100644 index 000000000..777ad2f0d --- /dev/null +++ b/src/Attributes/Webhook.php @@ -0,0 +1,34 @@ +|null $x + * @param Attachable[]|null $attachables + */ + public function __construct( + ?string $name = null, + ?PathItem $pathItem = null, + // annotation + ?array $x = null, + ?array $attachables = null + ) { + parent::__construct([ + 'name' => $name ?? Generator::UNDEFINED, + 'pathItem' => $pathItem ?? Generator::UNDEFINED, + 'x' => $x ?? Generator::UNDEFINED, + 'value' => $this->combine($name, $pathItem, $attachables), + ]); + } +} diff --git a/src/Serializer.php b/src/Serializer.php index 71f9b2b93..d8b59d359 100644 --- a/src/Serializer.php +++ b/src/Serializer.php @@ -58,6 +58,7 @@ class Serializer OA\Trace::class, OA\Xml::class, OA\XmlContent::class, + OA\Webhook::class, ]; protected static function isValidAnnotationClass(string $className): bool diff --git a/tests/Annotations/OpenApiTest.php b/tests/Annotations/OpenApiTest.php index 33e48a171..95458d907 100644 --- a/tests/Annotations/OpenApiTest.php +++ b/tests/Annotations/OpenApiTest.php @@ -13,7 +13,7 @@ class OpenApiTest extends OpenApiTestCase { public function testValidVersion310(): void { - $this->assertOpenApiLogEntryContains('The OpenAPI document must contain at least one paths field, a components field or a webhooks field'); + $this->assertOpenApiLogEntryContains("At least one of 'Required @OA\Info(), @OA\Components() or @OA\Webhook() not found'"); $openapi = new OA\OpenApi(['_context' => $this->getContext()]); $openapi->openapi = '3.1.0'; @@ -22,7 +22,7 @@ public function testValidVersion310(): void public function testValidVersion300(): void { - $this->assertOpenApiLogEntryContains('The OpenAPI document must contain paths field'); + $this->assertOpenApiLogEntryContains('Required @OA\PathItem() not found'); $openapi = new OA\OpenApi(['_context' => $this->getContext()]); $openapi->openapi = '3.0.0'; diff --git a/tests/ContextTest.php b/tests/ContextTest.php index d6e3cea89..4604b8679 100644 --- a/tests/ContextTest.php +++ b/tests/ContextTest.php @@ -26,7 +26,7 @@ public function testDetect(): void public function testFullyQualifiedName(): void { - $this->assertOpenApiLogEntryContains('The OpenAPI document must contain paths field'); + $this->assertOpenApiLogEntryContains('Required @OA\PathItem() not found'); $openapi = (new Generator($this->getTrackingLogger())) ->setAnalyser(new TokenAnalyser()) ->generate([$this->fixture('Customer.php')]); diff --git a/tests/GeneratorTest.php b/tests/GeneratorTest.php index 3853cbade..4cdc653b3 100644 --- a/tests/GeneratorTest.php +++ b/tests/GeneratorTest.php @@ -38,7 +38,7 @@ public function testScan(string $sourceDir, iterable $sources): void public function testScanInvalidSource(): void { $this->assertOpenApiLogEntryContains('Skipping invalid source: /tmp/__swagger_php_does_not_exist__'); - $this->assertOpenApiLogEntryContains('The OpenAPI document must contain paths field'); + $this->assertOpenApiLogEntryContains('Required @OA\PathItem() not found'); (new Generator($this->getTrackingLogger())) ->setAnalyser($this->getAnalyzer()) From e63c24e325531afb9396fceff48f4880ee046635 Mon Sep 17 00:00:00 2001 From: Quentin Dequippe Date: Mon, 11 Sep 2023 10:03:39 +0400 Subject: [PATCH 7/7] add Webhook to nested type OpenAPI --- src/Annotations/OpenApi.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Annotations/OpenApi.php b/src/Annotations/OpenApi.php index 0be0cd72c..617fed259 100644 --- a/src/Annotations/OpenApi.php +++ b/src/Annotations/OpenApi.php @@ -122,6 +122,7 @@ class OpenApi extends AbstractAnnotation Info::class => 'info', Server::class => ['servers'], PathItem::class => ['paths', 'path'], + Webhook::class => ['webhooks'], Components::class => 'components', Tag::class => ['tags'], ExternalDocumentation::class => 'externalDocs',