From 2d0ab104b0fdd7e04428736f46ff612c43fdb03f Mon Sep 17 00:00:00 2001 From: Zbigniew Malcherczyk Date: Sun, 28 Jan 2024 23:56:43 +0100 Subject: [PATCH] chore: enable contract between documentation strategy (#16) Quite a smart-ish improvement when instead of an array as contract (a bad one, but flexible), we can reuse the Message and Property attributes, so now instead of array-ing properties, we just add extra property either via Reflection or via an attribute. --- src/Attribute/Message.php | 7 +++- src/DocumentationEditor.php | 2 +- .../AttributeDocumentationStrategy.php | 6 ++-- .../DocumentationStrategyInterface.php | 4 ++- .../ReflectionDocumentationStrategy.php | 32 ++++++++++++------- src/Schema/PropertyType.php | 10 ++++++ src/Schema/V2/SchemaRenderer.php | 1 + .../Console/DumpSpecificationConsole.php | 2 +- .../AttributeDocumentationStrategyTest.php | 8 +++-- .../ReflectionDocumentationStrategyTest.php | 30 +++++++++++------ 10 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/Attribute/Message.php b/src/Attribute/Message.php index 28b5f40..c7802f8 100644 --- a/src/Attribute/Message.php +++ b/src/Attribute/Message.php @@ -16,7 +16,7 @@ class Message implements PropertyInterface public function __construct( public readonly string $name, public readonly string $channel, - public readonly array $properties = [], + public array $properties = [], public readonly ChannelType $channelType = ChannelType::SUBSCRIBE, ) { } @@ -30,4 +30,9 @@ public function toArray(): array 'channelType' => $this->channelType->value, ]; } + + public function addProperty(PropertyInterface $property): void + { + $this->properties[] = $property; + } } diff --git a/src/DocumentationEditor.php b/src/DocumentationEditor.php index cafbc13..a0a3a60 100644 --- a/src/DocumentationEditor.php +++ b/src/DocumentationEditor.php @@ -26,7 +26,7 @@ public function document(string $class): array } foreach ($strategies as $documentationStrategy) { - $result = array_merge($result, $documentationStrategy->document($class)); + $result = array_merge($result, $documentationStrategy->document($class)->toArray()); } return $result; diff --git a/src/DocumentationStrategy/AttributeDocumentationStrategy.php b/src/DocumentationStrategy/AttributeDocumentationStrategy.php index 300d3da..2a075e0 100644 --- a/src/DocumentationStrategy/AttributeDocumentationStrategy.php +++ b/src/DocumentationStrategy/AttributeDocumentationStrategy.php @@ -15,7 +15,7 @@ public function __construct( ) { } - public function document(string $class): array + public function document(string $class): Message { $reflection = new ReflectionClass($class); /** @var ReflectionAttribute[] $messageAttributes */ @@ -25,10 +25,10 @@ public function document(string $class): array throw new DocumentationStrategyException('Error: class ' . $class . ' must have at least ' . Message::class . ' attribute.'); } - $message = $messageAttributes[0]->newInstance()->toArray(); + $message = $messageAttributes[0]->newInstance(); foreach ($this->propertyExtractor->extract($class) as $property) { - $message['properties'][] = $property->toArray(); + $message->addProperty($property); } return $message; diff --git a/src/DocumentationStrategy/DocumentationStrategyInterface.php b/src/DocumentationStrategy/DocumentationStrategyInterface.php index 526217f..f3e8c27 100644 --- a/src/DocumentationStrategy/DocumentationStrategyInterface.php +++ b/src/DocumentationStrategy/DocumentationStrategyInterface.php @@ -4,10 +4,12 @@ namespace Ferror\AsyncapiDocBundle\DocumentationStrategy; +use Ferror\AsyncapiDocBundle\Attribute\Message; + interface DocumentationStrategyInterface { /** * @param class-string $class */ - public function document(string $class): array; + public function document(string $class): Message; } diff --git a/src/DocumentationStrategy/ReflectionDocumentationStrategy.php b/src/DocumentationStrategy/ReflectionDocumentationStrategy.php index 1883781..53890ff 100644 --- a/src/DocumentationStrategy/ReflectionDocumentationStrategy.php +++ b/src/DocumentationStrategy/ReflectionDocumentationStrategy.php @@ -4,6 +4,10 @@ namespace Ferror\AsyncapiDocBundle\DocumentationStrategy; +use Ferror\AsyncapiDocBundle\Attribute\Message; +use Ferror\AsyncapiDocBundle\Attribute\Property; +use Ferror\AsyncapiDocBundle\Schema\PropertyType; +use ReflectionAttribute; use ReflectionClass; use ReflectionException; use ReflectionNamedType; @@ -14,27 +18,33 @@ * @param class-string $class * * @throws ReflectionException + * @throws DocumentationStrategyException */ - public function document(string $class): array + public function document(string $class): Message { $reflection = new ReflectionClass($class); - $properties = $reflection->getProperties(); + /** @var ReflectionAttribute[] $messageAttributes */ + $messageAttributes = $reflection->getAttributes(Message::class); + + if (empty($messageAttributes)) { + throw new DocumentationStrategyException('Error: class ' . $class . ' must have at least ' . Message::class . ' attribute.'); + } - $message['name'] = $reflection->getShortName(); + $message = $messageAttributes[0]->newInstance(); + $properties = $reflection->getProperties(); foreach ($properties as $property) { /** @var ReflectionNamedType|null $type */ $type = $property->getType(); $name = $property->getName(); - if ($type && !$type->allowsNull()) { - $message['required'][] = $name; - } - - $message['properties'][] = [ - 'name' => $name, - 'type' => $type?->getName(), - ]; + $message->addProperty( + new Property( + name: $name, + type: PropertyType::fromNative($type?->getName()), + required: $type && !$type->allowsNull(), + ) + ); } return $message; diff --git a/src/Schema/PropertyType.php b/src/Schema/PropertyType.php index 69554b1..a4d08e7 100644 --- a/src/Schema/PropertyType.php +++ b/src/Schema/PropertyType.php @@ -13,4 +13,14 @@ enum PropertyType: string case BOOLEAN = 'boolean'; case INTEGER = 'integer'; case FLOAT = 'number'; + + public static function fromNative(string $type): self + { + return match ($type) { + 'bool', 'boolean' => self::BOOLEAN, + 'int', 'integer' => self::INTEGER, + 'float', 'number' => self::FLOAT, + default => self::STRING, + }; + } } diff --git a/src/Schema/V2/SchemaRenderer.php b/src/Schema/V2/SchemaRenderer.php index ca2285c..824ebb2 100644 --- a/src/Schema/V2/SchemaRenderer.php +++ b/src/Schema/V2/SchemaRenderer.php @@ -30,6 +30,7 @@ public function generate(): array foreach ($classes as $class) { $document = $this->documentationStrategy->document($class); + $document = $document->toArray(); $channel = $this->channelRenderer->render($document); $message = $this->messageRenderer->render($document); diff --git a/src/Symfony/Console/DumpSpecificationConsole.php b/src/Symfony/Console/DumpSpecificationConsole.php index a601920..6fefaac 100644 --- a/src/Symfony/Console/DumpSpecificationConsole.php +++ b/src/Symfony/Console/DumpSpecificationConsole.php @@ -36,7 +36,7 @@ public function execute(InputInterface $input, OutputInterface $output): int if ($input->getArgument('class')) { $document = $this->documentationStrategy->document($input->getArgument('class')); - $schema = $this->messageRenderer->render($document); + $schema = $this->messageRenderer->render($document->toArray()); $io->writeln(Yaml::dump($schema, 10, 2)); diff --git a/tests/Unit/DocumentationStrategy/AttributeDocumentationStrategyTest.php b/tests/Unit/DocumentationStrategy/AttributeDocumentationStrategyTest.php index 8fe18ec..5c3d689 100644 --- a/tests/Unit/DocumentationStrategy/AttributeDocumentationStrategyTest.php +++ b/tests/Unit/DocumentationStrategy/AttributeDocumentationStrategyTest.php @@ -16,6 +16,8 @@ public function testUserSignedUp(): void { $documentation = new AttributeDocumentationStrategy(new PropertyExtractor()); + $actual = $documentation->document(UserSignedUp::class)->toArray(); + $expected = [ 'name' => 'UserSignedUp', 'channel' => 'user_signed_up', @@ -56,13 +58,15 @@ public function testUserSignedUp(): void ], ]; - $this->assertEquals($expected, $documentation->document(UserSignedUp::class)); + $this->assertEquals($expected, $actual); } public function testProductCreated(): void { $documentation = new AttributeDocumentationStrategy(new PropertyExtractor()); + $actual = $documentation->document(ProductCreated::class)->toArray(); + $expected = [ 'name' => 'ProductCreated', 'channel' => 'product.created', @@ -144,6 +148,6 @@ public function testProductCreated(): void ], ]; - $this->assertEquals($expected, $documentation->document(ProductCreated::class)); + $this->assertEquals($expected, $actual); } } diff --git a/tests/Unit/DocumentationStrategy/ReflectionDocumentationStrategyTest.php b/tests/Unit/DocumentationStrategy/ReflectionDocumentationStrategyTest.php index 53359eb..613bf78 100644 --- a/tests/Unit/DocumentationStrategy/ReflectionDocumentationStrategyTest.php +++ b/tests/Unit/DocumentationStrategy/ReflectionDocumentationStrategyTest.php @@ -16,32 +16,44 @@ public function test(): void $expected = [ 'name' => 'UserSignedUp', + 'channel' => 'user_signed_up', + 'channelType' => 'subscribe', 'properties' => [ [ 'name' => 'name', 'type' => 'string', + 'required' => true, + 'description' => '', + 'format' => null, + 'example' => null, ], [ 'name' => 'email', 'type' => 'string', + 'required' => true, + 'description' => '', + 'format' => null, + 'example' => null, ], [ 'name' => 'age', - 'type' => 'int', + 'type' => 'integer', + 'required' => true, + 'description' => '', + 'format' => null, + 'example' => null, ], [ 'name' => 'isCitizen', - 'type' => 'bool', + 'type' => 'boolean', + 'required' => true, + 'description' => '', + 'format' => null, + 'example' => null, ], ], - 'required' => [ - 'name', - 'email', - 'age', - 'isCitizen', - ] ]; - $this->assertEquals($expected, $documentation->document(UserSignedUp::class)); + $this->assertEquals($expected, $documentation->document(UserSignedUp::class)->toArray()); } }