diff --git a/Examples/Readme.md b/Examples/Readme.md index d0c27904..b038e101 100644 --- a/Examples/Readme.md +++ b/Examples/Readme.md @@ -29,10 +29,12 @@ Collection of code/annotation examples and their corresponding OpenAPI specs gen * misc: [source](misc) / [spec](misc/misc.yaml) * using interfaces: [source](using-interfaces) / [spec](using-interfaces/using-interfaces.yaml) * using traits: [source](using-traits) / [spec](using-traits/using-traits.yaml) - * using refs: [source](using-refs) / [spec](using-refs/using-refs.yaml) + * using refs: [source](using-refs) / [spec](using-refs/using-refs.yaml) * nested schemas and class hierachies: [source](nesting) / [spec](nesting/nesting.yaml) * polymorphism using `@OA\Discriminator`: [source](polymorphism) / [spec](polymorphism/polymorphism.yaml) - + * webhooks using `@OA\Webhooks`: [source](webhooks) / [spec](webhooks/webhooks.yaml) + * webhooks81 using `@OAT\Webhooks`: [source](webhooks81) / [spec](webhooks81/webhooks.yaml) + ## Custom processors @@ -52,10 +54,10 @@ class MyCustomProcessor { public function __invoke(Analysis $analysis) { - // custom processing + // custom processing } } -``` +``` * **schema-query-parameter processor** @@ -66,6 +68,6 @@ class MyCustomProcessor * **sort-components processor** - A processor that sorts components so they appear in alphabetical order. + A processor that sorts components so they appear in alphabetical order. [source](processors/sort-components) diff --git a/Examples/webhooks/OpenApiSpec.php b/Examples/webhooks/OpenApiSpec.php new file mode 100644 index 00000000..02b901da --- /dev/null +++ b/Examples/webhooks/OpenApiSpec.php @@ -0,0 +1,34 @@ + 'components', Tag::class => ['tags'], ExternalDocumentation::class => 'externalDocs', + Webhook::class => ['webhooks', 'webhook'], Attachable::class => ['attachables'], ]; @@ -143,6 +151,21 @@ public function validate(array $stack = null, array $skip = null, string $ref = return false; } + /* paths is optional in 3.1.0 */ + if ($this->openapi === self::VERSION_3_0_0 && Generator::isDefault($this->paths)) { + $this->_context->logger->warning('Required @OA\PathItem() not found'); + } + + 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\PathItem(), @OA\Components() or @OA\Webhook() not found'"); + + return false; + } + return parent::validate([], [], '#', new \stdClass()); } @@ -230,4 +253,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..2ad40b40 --- /dev/null +++ b/src/Annotations/Webhook.php @@ -0,0 +1,43 @@ + 'string', + ]; +} diff --git a/src/Attributes/OpenApi.php b/src/Attributes/OpenApi.php index ce625e21..c1af0145 100644 --- a/src/Attributes/OpenApi.php +++ b/src/Attributes/OpenApi.php @@ -15,6 +15,7 @@ class OpenApi extends \OpenApi\Annotations\OpenApi * @param Server[]|null $servers * @param Tag[]|null $tags * @param PathItem[]|null $paths + * @param Webhook[]|null $webhooks * @param array|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, $webhooks, $attachables), ]); } } diff --git a/src/Attributes/Webhook.php b/src/Attributes/Webhook.php new file mode 100644 index 00000000..c759b6ef --- /dev/null +++ b/src/Attributes/Webhook.php @@ -0,0 +1,51 @@ +|null $x + * @param Attachable[]|null $attachables + */ + public function __construct( + ?string $webhook = null, + ?string $path = null, + mixed $ref = null, + ?string $summary = null, + ?string $description = null, + ?Get $get = null, + ?Put $put = null, + ?Post $post = null, + ?Delete $delete = null, + ?Options $options = null, + ?Head $head = null, + ?Patch $patch = null, + ?Trace $trace = null, + ?array $servers = null, + ?array $parameters = null, + // annotation + ?array $x = null, + ?array $attachables = null + ) { + parent::__construct([ + 'webhook' => $webhook ?? Generator::UNDEFINED, + 'path' => $path ?? Generator::UNDEFINED, + 'ref' => $ref ?? Generator::UNDEFINED, + 'summary' => $summary ?? Generator::UNDEFINED, + 'description' => $description ?? Generator::UNDEFINED, + 'x' => $x ?? Generator::UNDEFINED, + 'value' => $this->combine($get, $put, $post, $delete, $options, $head, $patch, $trace, $servers, $parameters, $attachables), + ]); + } +} diff --git a/src/Serializer.php b/src/Serializer.php index 71f9b2b9..52af1805 100644 --- a/src/Serializer.php +++ b/src/Serializer.php @@ -56,6 +56,7 @@ class Serializer OA\ServerVariable::class, OA\Tag::class, OA\Trace::class, + OA\Webhook::class, OA\Xml::class, OA\XmlContent::class, ]; diff --git a/tests/Annotations/OpenApiTest.php b/tests/Annotations/OpenApiTest.php index cf786264..1473608e 100644 --- a/tests/Annotations/OpenApiTest.php +++ b/tests/Annotations/OpenApiTest.php @@ -13,14 +13,23 @@ class OpenApiTest extends OpenApiTestCase { public function testValidVersion(): void { - $this->assertOpenApiLogEntryContains('Required @OA\Info() not found'); $this->assertOpenApiLogEntryContains('Required @OA\PathItem() not found'); + $this->assertOpenApiLogEntryContains('Required @OA\Info() not found'); $openapi = new OA\OpenApi(['_context' => $this->getContext()]); $openapi->openapi = '3.0.0'; $openapi->validate(); } + public function testValidVersion310(): void + { + $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->validate(); + } + public function testInvalidVersion(): void { $this->assertOpenApiLogEntryContains('Unsupported OpenAPI version "2". Allowed versions are: 3.0.0, 3.1.0'); diff --git a/tests/ExamplesTest.php b/tests/ExamplesTest.php index 239ac243..db724d25 100644 --- a/tests/ExamplesTest.php +++ b/tests/ExamplesTest.php @@ -21,132 +21,166 @@ class ExamplesTest extends OpenApiTestCase public function exampleDetails(): iterable { yield 'example-object' => [ - OA\OpenApi::VERSION_3_0_0, - 'example-object', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'example-object', 'example-object.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token', 'reflection'], ]; yield 'misc' => [ - OA\OpenApi::VERSION_3_0_0, - 'misc', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'misc', 'misc.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token', 'reflection'], ]; yield 'nesting' => [ - OA\OpenApi::VERSION_3_0_0, - 'nesting', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'nesting', 'nesting.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token', 'reflection'], ]; yield 'petstore-3.0' => [ - OA\OpenApi::VERSION_3_0_0, - 'petstore-3.0', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'petstore-3.0', 'petstore-3.0.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token', 'reflection'], ]; yield 'petstore.swagger.io' => [ - OA\OpenApi::VERSION_3_0_0, - 'petstore.swagger.io', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'petstore.swagger.io', 'petstore.swagger.io.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token', 'reflection'], ]; yield 'swagger-spec/petstore' => [ - OA\OpenApi::VERSION_3_0_0, - 'swagger-spec/petstore', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'swagger-spec/petstore', 'petstore.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token', 'reflection'], ]; yield 'swagger-spec/petstore-simple' => [ - OA\OpenApi::VERSION_3_0_0, - 'swagger-spec/petstore-simple', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'swagger-spec/petstore-simple', 'petstore-simple.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token', 'reflection'], ]; yield 'swagger-spec/petstore-simple-3.1.0' => [ - OA\OpenApi::VERSION_3_1_0, - 'swagger-spec/petstore-simple', + 'version' => OA\OpenApi::VERSION_3_1_0, + 'example' => 'swagger-spec/petstore-simple', 'petstore-simple-3.1.0.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token', 'reflection'], ]; yield 'swagger-spec/petstore-with-external-docs' => [ - OA\OpenApi::VERSION_3_0_0, - 'swagger-spec/petstore-with-external-docs', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'swagger-spec/petstore-with-external-docs', 'petstore-with-external-docs.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token', 'reflection'], ]; yield 'polymorphism' => [ - OA\OpenApi::VERSION_3_0_0, - 'polymorphism', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'polymorphism', 'polymorphism.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['reflection'], ]; yield 'polymorphism-3.1.0' => [ - OA\OpenApi::VERSION_3_1_0, - 'polymorphism', + 'version' => OA\OpenApi::VERSION_3_1_0, + 'example' => 'polymorphism', 'polymorphism-3.1.0.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['reflection'], ]; yield 'using-interfaces' => [ - OA\OpenApi::VERSION_3_0_0, - 'using-interfaces', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'using-interfaces', 'using-interfaces.yaml', - false, - [], - ]; - - yield 'using-refs' => [ - OA\OpenApi::VERSION_3_0_0, - 'using-refs', - 'using-refs.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token', 'reflection'], ]; yield 'using-traits' => [ - OA\OpenApi::VERSION_3_0_0, - 'using-traits', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'using-traits', 'using-traits.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token', 'reflection'], ]; yield 'using-links' => [ - OA\OpenApi::VERSION_3_0_0, - 'using-links', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'using-links', 'using-links.yaml', - false, - [], + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token', 'reflection'], ]; if (\PHP_VERSION_ID >= 80100) { + yield 'using-refs' => [ + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'using-refs', + 'using-refs.yaml', + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['reflection'], + ]; + + yield 'webhooks' => [ + 'version' => OA\OpenApi::VERSION_3_1_0, + 'example' => 'webhooks', + 'webhooks.yaml', + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['token','reflection'], + ]; + + yield 'webhooks81' => [ + 'version' => OA\OpenApi::VERSION_3_1_0, + 'example' => 'webhooks81', + 'webhooks.yaml', + 'debug' => false, + 'expectedLog' => [], + 'analysers' => ['reflection'], + ]; + yield 'using-links-php81' => [ - OA\OpenApi::VERSION_3_0_0, - 'using-links-php81', + 'version' => OA\OpenApi::VERSION_3_0_0, + 'example' => 'using-links-php81', 'using-links-php81.yaml', - true, - ['JetBrains\PhpStorm\ArrayShape'], + 'debug' => true, + 'expectedLog' => ['JetBrains\PhpStorm\ArrayShape'], + 'analysers' => ['reflection'], ]; } } @@ -158,18 +192,11 @@ public function exampleMappings(): iterable 'reflection' => new ReflectionAnalyser([new DocBlockAnnotationFactory(), new AttributeAnnotationFactory()]), ]; - foreach ($this->exampleDetails() as $eKey => $example) { - foreach ($analysers as $aKey => $analyser) { - if (0 === strpos($eKey, 'polymorphism') && 'token' == $aKey) { - continue; - } - if ((\PHP_VERSION_ID < 80100 || 'token' == $aKey) && 'using-refs' == $eKey) { - continue; - } - if ('using-links-php81' == $eKey && 'token' == $aKey) { - continue; - } - yield $eKey . ':' . $aKey => array_merge($example, [$analyser]); + foreach ($this->exampleDetails() as $exampleKey => $example) { + $exampleAnalysers = $example['analysers']; + unset($example['analysers']); + foreach ($exampleAnalysers as $analyserKey) { + yield $exampleKey . ':' . $analyserKey => array_merge($example, [$analysers[$analyserKey]]); } } } diff --git a/tests/GeneratorTest.php b/tests/GeneratorTest.php index 3dbd1c3f..be8c4333 100644 --- a/tests/GeneratorTest.php +++ b/tests/GeneratorTest.php @@ -38,8 +38,8 @@ 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('Required @OA\Info() not found'); (new Generator($this->getTrackingLogger())) ->setAnalyser($this->getAnalyzer())