diff --git a/Examples/webhook-example/OpenApiSpec.php b/Examples/webhook-example/OpenApiSpec.php new file mode 100644 index 00000000..5ce29887 --- /dev/null +++ b/Examples/webhook-example/OpenApiSpec.php @@ -0,0 +1,34 @@ + Individual operations can override this definition.
To make security optional, an empty security requirement `({})` can be included in the array.

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

The available webhooks for the API.

#### Reference diff --git a/docs/reference/attributes.md b/docs/reference/attributes.md index c4ebcf0c..ac23680a 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|string|class-string|object>|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 74775276..617fed25 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 Webhook[] + */ + public $webhooks = Generator::UNDEFINED; + /** * @var Analysis */ @@ -106,7 +113,7 @@ class OpenApi extends AbstractAnnotation /** * @inheritdoc */ - public static $_required = ['openapi', 'info', 'paths']; + public static $_required = ['openapi', 'info']; /** * @inheritdoc @@ -115,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', @@ -143,6 +151,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('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("At least one of 'Required @OA\Info(), @OA\Components() or @OA\Webhook() not found'"); + + return false; + } + return parent::validate([], [], '#', new \stdClass()); } @@ -230,4 +250,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/Annotations/Webhook.php b/src/Annotations/Webhook.php new file mode 100644 index 00000000..4fb581f0 --- /dev/null +++ b/src/Annotations/Webhook.php @@ -0,0 +1,25 @@ +|null $x * @param Attachable[]|null $attachables */ @@ -27,6 +28,7 @@ public function __construct( ?ExternalDocumentation $externalDocs = null, ?array $paths = null, ?Components $components = null, + ?array $webhooks = null, // annotation ?array $x = null, ?array $attachables = null @@ -35,7 +37,7 @@ public function __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), ]); } } diff --git a/src/Attributes/Webhook.php b/src/Attributes/Webhook.php new file mode 100644 index 00000000..777ad2f0 --- /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 71f9b2b9..d8b59d35 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 cf786264..95458d90 100644 --- a/tests/Annotations/OpenApiTest.php +++ b/tests/Annotations/OpenApiTest.php @@ -11,9 +11,17 @@ class OpenApiTest extends OpenApiTestCase { - public function testValidVersion(): void + public function testValidVersion310(): void + { + $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'; + $openapi->validate(); + } + + public function testValidVersion300(): void { - $this->assertOpenApiLogEntryContains('Required @OA\Info() not found'); $this->assertOpenApiLogEntryContains('Required @OA\PathItem() not found'); $openapi = new OA\OpenApi(['_context' => $this->getContext()]); diff --git a/tests/ExamplesTest.php b/tests/ExamplesTest.php index 239ac243..f9215097 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, diff --git a/tests/GeneratorTest.php b/tests/GeneratorTest.php index 3dbd1c3f..4cdc653b 100644 --- a/tests/GeneratorTest.php +++ b/tests/GeneratorTest.php @@ -38,7 +38,6 @@ 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'); (new Generator($this->getTrackingLogger()))