Skip to content

Commit

Permalink
feat: add webhook - openapi (#5873)
Browse files Browse the repository at this point in the history
* feat: add webhook - openapi

* cs

---------

Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>
  • Loading branch information
alli83 and soyuka committed Mar 20, 2024
1 parent 5523bf5 commit 125f2ce
Show file tree
Hide file tree
Showing 14 changed files with 181 additions and 21 deletions.
5 changes: 5 additions & 0 deletions features/openapi/docs.feature
Expand Up @@ -70,6 +70,7 @@ Feature: Documentation support
And the OpenAPI class "UuidIdentifierDummy" exists
And the OpenAPI class "ThirdLevel" exists
And the OpenAPI class "DummyCar" exists
And the OpenAPI class "DummyWebhook" exists
And the OpenAPI class "ParentDummy" doesn't exist
And the OpenAPI class "UnknownDummy" doesn't exist
And the OpenAPI path "/relation_embedders/{id}/custom" exists
Expand Down Expand Up @@ -115,6 +116,10 @@ Feature: Documentation support
And the JSON node "paths./dummy_cars.get.parameters[8].name" should be equal to "foobar[]"
And the JSON node "paths./dummy_cars.get.parameters[8].description" should be equal to "Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: foobar[]={propertyName}&foobar[]={anotherPropertyName}&foobar[{nestedPropertyParent}][]={nestedProperty}"

# Webhook
And the JSON node "webhooks.webhook[0].get.description" should be equal to "Something else here for example"
And the JSON node "webhooks.webhook[1].post.description" should be equal to "Hi! it's me, I'm the problem, it's me"

# Subcollection - check filter on subResource
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].name" should be equal to "id"
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].in" should be equal to "path"
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/Delete.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/Error.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/Get.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/GetCollection.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
7 changes: 4 additions & 3 deletions src/Metadata/HttpOperation.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;
use Symfony\Component\WebLink\Link as WebLink;
Expand Down Expand Up @@ -150,7 +151,7 @@ public function __construct(
protected ?array $paginationViaCursor = null,
protected ?array $hydraContext = null,
protected ?array $openapiContext = null, // TODO Remove in 4.0
protected bool|OpenApiOperation|null $openapi = null,
protected bool|OpenApiOperation|Webhook|null $openapi = null,
protected ?array $exceptionToStatus = null,
protected ?bool $queryParameterValidationEnabled = null,
protected ?array $links = null,
Expand Down Expand Up @@ -578,12 +579,12 @@ public function withOpenapiContext(array $openapiContext): self
return $self;
}

public function getOpenapi(): bool|OpenApiOperation|null
public function getOpenapi(): bool|OpenApiOperation|Webhook|null
{
return $this->openapi;
}

public function withOpenapi(bool|OpenApiOperation $openapi): self
public function withOpenapi(bool|OpenApiOperation|Webhook $openapi): self
{
$self = clone $this;
$self->openapi = $openapi;
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/NotExposed.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -56,7 +57,7 @@ public function __construct(

?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = false,
bool|OpenApiOperation|Webhook|null $openapi = false,
?array $exceptionToStatus = null,

?bool $queryParameterValidationEnabled = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/Patch.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/Post.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Metadata/Put.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
?array $paginationViaCursor = null,
?array $hydraContext = null,
?array $openapiContext = null,
bool|OpenApiOperation|null $openapi = null,
bool|OpenApiOperation|Webhook|null $openapi = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
Expand Down
51 changes: 51 additions & 0 deletions src/OpenApi/Attributes/Webhook.php
@@ -0,0 +1,51 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\OpenApi\Attributes;

use ApiPlatform\OpenApi\Model\PathItem;

class Webhook
{
public function __construct(
protected string $name,
protected ?PathItem $pathItem = null,
) {
}

public function getName(): string
{
return $this->name;
}

public function withName(string $name): self
{
$self = clone $this;
$self->name = $name;

return $self;
}

public function getPathItem(): ?PathItem
{
return $this->pathItem;
}

public function withPathItem(PathItem $pathItem): self
{
$self = clone $this;
$self->pathItem = $pathItem;

return $self;
}
}
38 changes: 29 additions & 9 deletions src/OpenApi/Factory/OpenApiFactory.php
Expand Up @@ -26,6 +26,7 @@
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model;
use ApiPlatform\OpenApi\Model\Components;
use ApiPlatform\OpenApi\Model\Contact;
Expand Down Expand Up @@ -90,12 +91,13 @@ public function __invoke(array $context = []): OpenApi
$servers = '/' === $baseUrl || '' === $baseUrl ? [new Server('/')] : [new Server($baseUrl)];
$paths = new Paths();
$schemas = new \ArrayObject();
$webhooks = new \ArrayObject();

foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
$resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);

foreach ($resourceMetadataCollection as $resourceMetadata) {
$this->collectPaths($resourceMetadata, $resourceMetadataCollection, $paths, $schemas);
$this->collectPaths($resourceMetadata, $resourceMetadataCollection, $paths, $schemas, $webhooks);
}
}

Expand All @@ -119,11 +121,15 @@ public function __invoke(array $context = []): OpenApi
new \ArrayObject(),
new \ArrayObject($securitySchemes)
),
$securityRequirements
$securityRequirements,
[],
null,
null,
$webhooks
);
}

private function collectPaths(ApiResource $resource, ResourceMetadataCollection $resourceMetadataCollection, Paths $paths, \ArrayObject $schemas): void
private function collectPaths(ApiResource $resource, ResourceMetadataCollection $resourceMetadataCollection, Paths $paths, \ArrayObject $schemas, \ArrayObject $webhooks): void
{
if (0 === $resource->getOperations()->count()) {
return;
Expand All @@ -136,10 +142,10 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
continue;
}

$openapiOperation = $operation->getOpenapi();
$openapiAttribute = $operation->getOpenapi();

// Operation ignored from OpenApi
if ($operation instanceof HttpOperation && false === $openapiOperation) {
if ($operation instanceof HttpOperation && false === $openapiAttribute) {
continue;
}

Expand All @@ -163,8 +169,15 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
continue;
}

if (!\is_object($openapiOperation)) {
$pathItem = null;

if ($openapiAttribute instanceof Webhook) {
$pathItem = $openapiAttribute->getPathItem() ?: new PathItem();
$openapiOperation = $pathItem->{'get'.ucfirst(strtolower($method))}() ?: new Model\Operation();
} elseif (!\is_object($openapiAttribute)) {
$openapiOperation = new Model\Operation();
} else {
$openapiOperation = $openapiAttribute;
}

// Complete with defaults
Expand Down Expand Up @@ -230,7 +243,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection

if ($path) {
$pathItem = $paths->getPath($path) ?: new PathItem();
} else {
} elseif (!$pathItem) {
$pathItem = new PathItem();
}

Expand Down Expand Up @@ -391,7 +404,14 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
}
}

$paths->addPath($path, $pathItem->{'with'.ucfirst($method)}($openapiOperation));
if ($openapiAttribute instanceof Webhook) {
if (!isset($webhooks[$openapiAttribute->getName()])) {
$webhooks[$openapiAttribute->getName()] = new \ArrayObject();
}
$webhooks[$openapiAttribute->getName()]->append($pathItem->{'with'.ucfirst($method)}($openapiOperation));
} else {
$paths->addPath($path, $pathItem->{'with'.ucfirst($method)}($openapiOperation));
}
}
}

Expand Down Expand Up @@ -517,7 +537,7 @@ private function getLinks(ResourceMetadataCollection $resourceMetadataCollection
}

// Operation ignored from OpenApi
if ($operation instanceof HttpOperation && false === $operation->getOpenapi()) {
if ($operation instanceof HttpOperation && (false === $operation->getOpenapi() || $operation->getOpenapi() instanceof Webhook)) {
continue;
}

Expand Down
25 changes: 24 additions & 1 deletion src/OpenApi/Tests/Factory/OpenApiFactoryTest.php
Expand Up @@ -34,6 +34,7 @@
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\Metadata\Resource\ResourceNameCollection;
use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Factory\OpenApiFactory;
use ApiPlatform\OpenApi\Model;
use ApiPlatform\OpenApi\Model\Components;
Expand Down Expand Up @@ -79,6 +80,14 @@ public function testInvoke(): void
$baseOperation = (new HttpOperation())->withTypes(['http://schema.example.com/Dummy'])->withInputFormats(self::OPERATION_FORMATS['input_formats'])->withOutputFormats(self::OPERATION_FORMATS['output_formats'])->withClass(Dummy::class)->withOutput([
'class' => OutputDto::class,
])->withPaginationClientItemsPerPage(true)->withShortName('Dummy')->withDescription('This is a dummy');
$dummyResourceWebhook = (new ApiResource())->withOperations(new Operations([
'dummy webhook' => (new Get())->withUriTemplate('/dummy/{id}')->withShortName('short')->withOpenapi(new Webhook('happy webhook')),
'an other dummy webhook' => (new Post())->withUriTemplate('/dummies')->withShortName('short something')->withOpenapi(new Webhook('happy webhook', new Model\PathItem(post: new Operation(
summary: 'well...',
description: 'I dont\'t know what to say',
)))),
]));

$dummyResource = (new ApiResource())->withOperations(new Operations([
'ignored' => new NotExposed(),
'ignoredWithUriTemplate' => (new NotExposed())->withUriTemplate('/dummies/{id}'),
Expand Down Expand Up @@ -247,7 +256,7 @@ public function testInvoke(): void
$resourceNameCollectionFactoryProphecy->create()->shouldBeCalled()->willReturn(new ResourceNameCollection([Dummy::class]));

$resourceCollectionMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
$resourceCollectionMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [$dummyResource]));
$resourceCollectionMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [$dummyResource, $dummyResourceWebhook]));

$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id', 'name', 'description', 'dummyDate', 'enum']));
Expand Down Expand Up @@ -482,6 +491,20 @@ public function testInvoke(): void
$this->assertEquals($openApi->getInfo(), new Info('Test API', '1.2.3', 'This is a test API.'));
$this->assertEquals($openApi->getServers(), [new Server('/app_dev.php/')]);

$webhooks = $openApi->getWebhooks();
$this->assertCount(1, $webhooks);

$this->assertNotNull($webhooks['happy webhook']);
$this->assertCount(2, $webhooks['happy webhook']);

$firstOperationWebhook = $webhooks['happy webhook'][0];
$secondOperationWebhook = $webhooks['happy webhook'][1];

$this->assertSame('dummy webhook', $firstOperationWebhook->getGet()->getOperationId());
$this->assertSame('an other dummy webhook', $secondOperationWebhook->getPost()->getOperationId());
$this->assertSame('I dont\'t know what to say', $secondOperationWebhook->getPost()->getDescription());
$this->assertSame('well...', $secondOperationWebhook->getPost()->getSummary());

$components = $openApi->getComponents();
$this->assertInstanceOf(Components::class, $components);

Expand Down

0 comments on commit 125f2ce

Please sign in to comment.