From 34c460df1fa9e446b30f9826f4393bbce8ef2f0c Mon Sep 17 00:00:00 2001 From: Zbigniew Malcherczyk Date: Sat, 3 Feb 2024 21:25:14 +0100 Subject: [PATCH] feat: channel attribute extraction (#22) Part of migration to Schema V3. The channel should be extracted from message as there may be many channels for one message and in V3 version the new operation attribute is coming. --- README.md | 7 +- example/src/ProductCreated.php | 4 +- example/src/UserSignedUp.php | 4 +- phpunit.xml | 3 + src/Attribute/Channel.php | 26 ++++ src/Attribute/Message.php | 13 +- .../AttributeDocumentationStrategy.php | 9 ++ src/Schema/V2/ChannelRenderer.php | 18 ++- tests/Examples/PaymentExecuted.php | 4 +- tests/Examples/ProductCreated.php | 4 +- tests/Examples/ProductUpdated.php | 3 +- tests/Examples/UserSignedUp.php | 4 +- tests/Unit/Attribute/MessageTest.php | 8 +- tests/Unit/DocumentationEditorTest.php | 3 +- .../AttributeDocumentationStrategyTest.php | 16 ++- .../ReflectionDocumentationStrategyTest.php | 6 +- tests/Unit/Schema/V2/ChannelRendererTest.php | 40 ++++++ tests/Unit/Schema/V2/MessageRendererTest.php | 127 +++++++++--------- 18 files changed, 204 insertions(+), 95 deletions(-) create mode 100644 src/Attribute/Channel.php create mode 100644 tests/Unit/Schema/V2/ChannelRendererTest.php diff --git a/README.md b/README.md index 1864b09..42b2787 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,10 @@ ferror_asyncapi_doc_bundle_html: ```php use Ferror\AsyncapiDocBundle\Attribute\Message; +use Ferror\AsyncapiDocBundle\Attribute\Channel; -#[Message(name: 'ProductCreated', channel: 'product.created')] +#[Message(name: 'ProductCreated')] +#[Channel(name: 'product.created')] // optional final readonly class ProductCreated { public function __construct( @@ -81,7 +83,8 @@ use Ferror\AsyncapiDocBundle\Attribute as AA; use Ferror\AsyncapiDocBundle\Schema\Format; use Ferror\AsyncapiDocBundle\Schema\PropertyType; -#[AA\Message(name: 'ProductCreated', channel: 'product.created')] +#[AA\Message(name: 'ProductCreated')] +#[AA\Channel(name: 'product.created')] // optional final readonly class ProductCreated { public function __construct( diff --git a/example/src/ProductCreated.php b/example/src/ProductCreated.php index 8a2cd53..faa6777 100644 --- a/example/src/ProductCreated.php +++ b/example/src/ProductCreated.php @@ -5,13 +5,15 @@ namespace App; use DateTime; +use Ferror\AsyncapiDocBundle\Attribute\Channel; use Ferror\AsyncapiDocBundle\Attribute\Message; use Ferror\AsyncapiDocBundle\Attribute\Property; use Ferror\AsyncapiDocBundle\Attribute\PropertyArray; use Ferror\AsyncapiDocBundle\Schema\Format; use Ferror\AsyncapiDocBundle\Schema\PropertyType; -#[Message(name: 'ProductCreated', channel: 'product.created')] +#[Message(name: 'ProductCreated')] +#[Channel(name: 'product.created')] final readonly class ProductCreated { /** diff --git a/example/src/UserSignedUp.php b/example/src/UserSignedUp.php index 14799fe..a3788f2 100644 --- a/example/src/UserSignedUp.php +++ b/example/src/UserSignedUp.php @@ -4,9 +4,11 @@ namespace App; +use Ferror\AsyncapiDocBundle\Attribute\Channel; use Ferror\AsyncapiDocBundle\Attribute\Message; -#[Message(name: 'UserSignedUp', channel: 'user_signed_up')] +#[Message(name: 'UserSignedUp')] +#[Channel(name: 'user_signed_up')] final readonly class UserSignedUp { public function __construct( diff --git a/phpunit.xml b/phpunit.xml index 5b70bd1..17f96cc 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -16,6 +16,9 @@ tests + + tests/Unit + diff --git a/src/Attribute/Channel.php b/src/Attribute/Channel.php new file mode 100644 index 0000000..b84fe61 --- /dev/null +++ b/src/Attribute/Channel.php @@ -0,0 +1,26 @@ + $this->name, + 'type' => $this->type->value, + ]; + } +} diff --git a/src/Attribute/Message.php b/src/Attribute/Message.php index a1046d5..73f8d98 100644 --- a/src/Attribute/Message.php +++ b/src/Attribute/Message.php @@ -5,19 +5,18 @@ namespace Ferror\AsyncapiDocBundle\Attribute; use Attribute; -use Ferror\AsyncapiDocBundle\Schema\V2\ChannelType; #[Attribute(Attribute::TARGET_CLASS)] class Message { /** * @param array $properties + * @param Channel[] $channels */ public function __construct( public readonly string $name, - public readonly string $channel, public array $properties = [], - public readonly ChannelType $channelType = ChannelType::SUBSCRIBE, + public array $channels = [], ) { } @@ -25,9 +24,8 @@ public function toArray(): array { return [ 'name' => $this->name, - 'channel' => $this->channel, 'properties' => array_map(static fn(PropertyInterface $property) => $property->toArray(), $this->properties), - 'channelType' => $this->channelType->value, + 'channels' => array_map(static fn(Channel $channel) => $channel->toArray(), $this->channels), ]; } @@ -36,6 +34,11 @@ public function addProperty(Property|PropertyArray|PropertyEnum|PropertyObject|P $this->properties[] = $property; } + public function addChannel(Channel $channel): void + { + $this->channels[] = $channel; + } + public function enrich(self $self): self { // UPDATE EXISTING diff --git a/src/DocumentationStrategy/AttributeDocumentationStrategy.php b/src/DocumentationStrategy/AttributeDocumentationStrategy.php index 9eb8cf7..d3fe12b 100644 --- a/src/DocumentationStrategy/AttributeDocumentationStrategy.php +++ b/src/DocumentationStrategy/AttributeDocumentationStrategy.php @@ -4,6 +4,7 @@ namespace Ferror\AsyncapiDocBundle\DocumentationStrategy; +use Ferror\AsyncapiDocBundle\Attribute\Channel; use Ferror\AsyncapiDocBundle\Attribute\Message; use ReflectionAttribute; use ReflectionClass; @@ -41,6 +42,14 @@ public function document(string $class): Message $message->addProperty($property); } + // Channels are optional as it's possible to document just Messages. + /** @var ReflectionAttribute[] $messageAttributes */ + $channelAttributes = $reflection->getAttributes(Channel::class); + + foreach ($channelAttributes as $channelAttribute) { + $message->addChannel($channelAttribute->newInstance()); + } + return $message; } } diff --git a/src/Schema/V2/ChannelRenderer.php b/src/Schema/V2/ChannelRenderer.php index fa7ed96..08c0fb6 100644 --- a/src/Schema/V2/ChannelRenderer.php +++ b/src/Schema/V2/ChannelRenderer.php @@ -8,14 +8,18 @@ class ChannelRenderer { public function render(array $document): array { - $channel[$document['channel']] = [ - $document['channelType'] => [ - 'message' => [ - '$ref' => '#/components/messages/' . $document['name'], + $channels = []; + + foreach ($document['channels'] as $channel) { + $channels[$channel['name']] = [ + $channel['type'] => [ + 'message' => [ + '$ref' => '#/components/messages/' . $document['name'], + ], ], - ], - ]; + ]; + } - return $channel; + return $channels; } } diff --git a/tests/Examples/PaymentExecuted.php b/tests/Examples/PaymentExecuted.php index 984c5c2..7b8c794 100644 --- a/tests/Examples/PaymentExecuted.php +++ b/tests/Examples/PaymentExecuted.php @@ -4,12 +4,14 @@ namespace Ferror\AsyncapiDocBundle\Tests\Examples; +use Ferror\AsyncapiDocBundle\Attribute\Channel; use Ferror\AsyncapiDocBundle\Attribute\Message; /** * This class represents an example of documenting by ReflectionStrategy */ -#[Message(name: 'PaymentExecuted', channel: 'payment_executed')] +#[Message(name: 'PaymentExecuted')] +#[Channel(name: 'payment_executed')] final readonly class PaymentExecuted { public function __construct( diff --git a/tests/Examples/ProductCreated.php b/tests/Examples/ProductCreated.php index 74e5e84..39fa55f 100644 --- a/tests/Examples/ProductCreated.php +++ b/tests/Examples/ProductCreated.php @@ -5,6 +5,7 @@ namespace Ferror\AsyncapiDocBundle\Tests\Examples; use DateTime; +use Ferror\AsyncapiDocBundle\Attribute\Channel; use Ferror\AsyncapiDocBundle\Attribute\Message; use Ferror\AsyncapiDocBundle\Attribute\Property; use Ferror\AsyncapiDocBundle\Attribute\PropertyArray; @@ -17,7 +18,8 @@ /** * This class represents an example of documenting by AttributeStrategy. It contains all types. */ -#[Message(name: 'ProductCreated', channel: 'product.created')] +#[Message(name: 'ProductCreated')] +#[Channel(name: 'product.created')] final readonly class ProductCreated { /** diff --git a/tests/Examples/ProductUpdated.php b/tests/Examples/ProductUpdated.php index 7553f34..ba60b22 100644 --- a/tests/Examples/ProductUpdated.php +++ b/tests/Examples/ProductUpdated.php @@ -5,6 +5,7 @@ namespace Ferror\AsyncapiDocBundle\Tests\Examples; use DateTime; +use Ferror\AsyncapiDocBundle\Attribute\Channel; use Ferror\AsyncapiDocBundle\Attribute\Message; use Ferror\AsyncapiDocBundle\Attribute\Property; use Ferror\AsyncapiDocBundle\Attribute\PropertyArray; @@ -19,7 +20,6 @@ */ #[Message( name: 'ProductUpdated', - channel: 'product.updated', properties: [ new Property(name: 'id', type: PropertyType::INTEGER), new Property(name: 'amount', type: PropertyType::FLOAT), @@ -32,6 +32,7 @@ new PropertyArray(name: 'tags', itemsType: PropertyType::STRING), ], )] +#[Channel(name: 'product.updated')] final readonly class ProductUpdated { /** diff --git a/tests/Examples/UserSignedUp.php b/tests/Examples/UserSignedUp.php index 0854989..1b61f42 100644 --- a/tests/Examples/UserSignedUp.php +++ b/tests/Examples/UserSignedUp.php @@ -4,6 +4,7 @@ namespace Ferror\AsyncapiDocBundle\Tests\Examples; +use Ferror\AsyncapiDocBundle\Attribute\Channel; use Ferror\AsyncapiDocBundle\Attribute\Message; use Ferror\AsyncapiDocBundle\Attribute\Property; use Ferror\AsyncapiDocBundle\Schema\Format; @@ -12,7 +13,8 @@ /** * This class represents a SIMPLE example of documenting by AttributeStrategy. */ -#[Message(name: 'UserSignedUp', channel: 'user_signed_up')] +#[Message(name: 'UserSignedUp')] +#[Channel(name: 'user_signed_up')] final readonly class UserSignedUp { public function __construct( diff --git a/tests/Unit/Attribute/MessageTest.php b/tests/Unit/Attribute/MessageTest.php index fa48eea..d545b63 100644 --- a/tests/Unit/Attribute/MessageTest.php +++ b/tests/Unit/Attribute/MessageTest.php @@ -12,18 +12,18 @@ final class MessageTest extends TestCase { public function testEnrichAddsProperty(): void { - $message = new Message('name', 'channel'); + $message = new Message('name'); - $message->enrich(new Message('name', 'channel', [new Property('name')])); + $message->enrich(new Message('name', [new Property('name')])); $this->assertCount(1, $message->properties); } public function testEnrichUpdatesProperty(): void { - $message = new Message('name', 'channel', [new Property('name')]); + $message = new Message('name', [new Property('name')]); - $message->enrich(new Message('name', 'channel', [new Property('name', 'Nice Description')])); + $message->enrich(new Message('name', [new Property('name', 'Nice Description')])); $this->assertEquals('Nice Description', $message->properties[0]->description); } diff --git a/tests/Unit/DocumentationEditorTest.php b/tests/Unit/DocumentationEditorTest.php index cb46f87..b75b995 100644 --- a/tests/Unit/DocumentationEditorTest.php +++ b/tests/Unit/DocumentationEditorTest.php @@ -24,8 +24,6 @@ public function testDocument(): void $expected = [ 'name' => 'PaymentExecuted', - 'channel' => 'payment_executed', - 'channelType' => 'subscribe', 'properties' => [ [ 'name' => 'amount', @@ -44,6 +42,7 @@ public function testDocument(): void 'example' => null, ], ], + 'channels' => [], ]; $this->assertEquals($expected, $actual); diff --git a/tests/Unit/DocumentationStrategy/AttributeDocumentationStrategyTest.php b/tests/Unit/DocumentationStrategy/AttributeDocumentationStrategyTest.php index 5c3d689..3f723da 100644 --- a/tests/Unit/DocumentationStrategy/AttributeDocumentationStrategyTest.php +++ b/tests/Unit/DocumentationStrategy/AttributeDocumentationStrategyTest.php @@ -20,8 +20,6 @@ public function testUserSignedUp(): void $expected = [ 'name' => 'UserSignedUp', - 'channel' => 'user_signed_up', - 'channelType' => 'subscribe', 'properties' => [ [ 'name' => 'name', @@ -56,6 +54,12 @@ public function testUserSignedUp(): void 'required' => true, ], ], + 'channels' => [ + [ + 'name' => 'user_signed_up', + 'type' => 'subscribe', + ], + ], ]; $this->assertEquals($expected, $actual); @@ -69,8 +73,6 @@ public function testProductCreated(): void $expected = [ 'name' => 'ProductCreated', - 'channel' => 'product.created', - 'channelType' => 'subscribe', 'properties' => [ [ 'name' => 'id', @@ -146,6 +148,12 @@ public function testProductCreated(): void 'required' => true, ], ], + 'channels' => [ + [ + 'name' => 'product.created', + 'type' => 'subscribe', + ], + ], ]; $this->assertEquals($expected, $actual); diff --git a/tests/Unit/DocumentationStrategy/ReflectionDocumentationStrategyTest.php b/tests/Unit/DocumentationStrategy/ReflectionDocumentationStrategyTest.php index 29abcc5..daf47b6 100644 --- a/tests/Unit/DocumentationStrategy/ReflectionDocumentationStrategyTest.php +++ b/tests/Unit/DocumentationStrategy/ReflectionDocumentationStrategyTest.php @@ -17,8 +17,6 @@ public function test(): void $expected = [ 'name' => 'UserSignedUp', - 'channel' => 'user_signed_up', - 'channelType' => 'subscribe', 'properties' => [ [ 'name' => 'name', @@ -53,6 +51,7 @@ public function test(): void 'example' => null, ], ], + 'channels' => [], ]; $this->assertEquals($expected, $documentation->document(UserSignedUp::class)->toArray()); @@ -64,8 +63,6 @@ public function testEnum(): void $expected = [ 'name' => 'ProductCreated', - 'channel' => 'product.created', - 'channelType' => 'subscribe', 'properties' => [ [ 'name' => 'id', @@ -100,6 +97,7 @@ public function testEnum(): void 'example' => null ], ], + 'channels' => [], ]; diff --git a/tests/Unit/Schema/V2/ChannelRendererTest.php b/tests/Unit/Schema/V2/ChannelRendererTest.php new file mode 100644 index 0000000..ad3d617 --- /dev/null +++ b/tests/Unit/Schema/V2/ChannelRendererTest.php @@ -0,0 +1,40 @@ + 'UserSignedUp', + 'channels' => [ + [ + 'name' => 'UserSignedUpChannel', + 'type' => 'subscribe', + ] + ] + ]; + + $schema = new ChannelRenderer(); + + $actual = $schema->render($document); + + $expected = [ + 'UserSignedUpChannel' => [ + 'subscribe' => [ + 'message' => [ + '$ref' => '#/components/messages/UserSignedUp', + ] + ] + ] + ]; + + $this->assertEquals($expected, $actual); + } +} diff --git a/tests/Unit/Schema/V2/MessageRendererTest.php b/tests/Unit/Schema/V2/MessageRendererTest.php index 53dccb9..7000b30 100644 --- a/tests/Unit/Schema/V2/MessageRendererTest.php +++ b/tests/Unit/Schema/V2/MessageRendererTest.php @@ -6,7 +6,6 @@ use Ferror\AsyncapiDocBundle\Schema\V2\MessageRenderer; use PHPUnit\Framework\TestCase; -use Symfony\Component\Yaml\Yaml; class MessageRendererTest extends TestCase { @@ -40,30 +39,29 @@ public function testReflection(): void $schema = new MessageRenderer(); - $specification = $schema->render($document); - - $expectedSpecification = <<assertEquals($expectedSpecification, Yaml::dump($specification, 10, 2)); + $actual = $schema->render($document); + + $expected = [ + 'UserSignedUp' => [ + 'payload' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'email' => ['type' => 'string'], + 'age' => ['type' => 'integer'], + 'isCitizen' => ['type' => 'boolean'], + ], + 'required' => [ + 'name', + 'email', + 'age', + 'isCitizen', + ], + ], + ], + ]; + + $this->assertEquals($expected, $actual); } public function testAttributes(): void @@ -108,42 +106,49 @@ public function testAttributes(): void $schema = new MessageRenderer(); - $specification = $schema->render($document); - - $expectedSpecification = <<assertEquals($expectedSpecification, Yaml::dump($specification, 10, 2)); + $actual = $schema->render($document); + + $expected = [ + 'UserSignedUp' => [ + 'payload' => [ + 'type' => 'object', + 'properties' => [ + 'name' => [ + 'type' => 'string', + 'description' => 'Name of the user', + 'format' => 'string', + 'example' => 'John', + ], + 'email' => [ + 'type' => 'string', + 'description' => 'Email of the user', + 'format' => 'email', + 'example' => 'john@example.com', + ], + 'age' => [ + 'type' => 'integer', + 'description' => 'Age of the user', + 'format' => 'int', + 'example' => '18', + ], + 'isCitizen' => [ + 'type' => 'boolean', + 'description' => 'Is user a citizen', + 'format' => 'boolean', + 'example' => 'true', + ], + ], + 'required' => [ + 'name', + 'email', + 'age', + 'isCitizen', + ], + ], + ], + ]; + + $this->assertEquals($expected, $actual); } public function testDoesNotRenderEmptyData(): void