diff --git a/AGENTS.md b/AGENTS.md index 4e18c861..4c43d9b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -100,7 +100,8 @@ Code Style ### Tests -- all tests classes should be a `final` +- tests classes should be a `final` +- tests should have at least one test group Module Structure ---------------- diff --git a/README.md b/README.md index a85f56fc..73d991c4 100644 --- a/README.md +++ b/README.md @@ -115,11 +115,12 @@ Usage Tests Definition Files and Transfer Object generators have been tested against the following APIs: * [NASA Open Api](https://api.nasa.gov/neo/rest/v1/neo/2465633?api_key=DEMO_KEY) -* [OpenWeather](https://openweathermap.org/current#example_JSON) -* [Content API for Shopping](https://developers.google.com/shopping-content/guides/products/products-api?hl=en) -* [Frankfurter is a free, open-source currency data API](https://api.frankfurter.dev/v1/latest) +* [OpenWeather](https://openweathermap.org/current?collection=current_forecast#example_JSON) +* [Google Content API for Shopping](https://developers.google.com/shopping-content/guides/products/products-api?hl=en) +* [Frankfurter - open-source currency data API](https://api.frankfurter.dev/v1/latest) * [Tagesschau API](https://tagesschau.api.bund.dev) * [Statistisches Bundesamt (Destatis)](https://www-genesis.destatis.de/genesisWS/swagger-ui/index.html#/find/findPost) +* [Wero - Digital Payment Wallet](https://developerhub.ppro.com/global-api/docs/wero) ### Scenario diff --git a/composer.lock b/composer.lock index f03819d9..b7268916 100644 --- a/composer.lock +++ b/composer.lock @@ -1464,11 +1464,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.38", + "version": "2.1.39", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629", - "reference": "dfaf1f530e1663aa167bc3e52197adb221582629", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6f73a2af4cbcd99c931d0fb8f08548cc0fa8224", + "reference": "c6f73a2af4cbcd99c931d0fb8f08548cc0fa8224", "shasum": "" }, "require": { @@ -1513,25 +1513,25 @@ "type": "github" } ], - "time": "2026-01-30T17:12:46+00:00" + "time": "2026-02-11T14:48:56+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.8", + "version": "2.0.10", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "1ed9e626a37f7067b594422411539aa807190573" + "reference": "1aba28b697c1e3b6bbec8a1725f8b11b6d3e5a5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/1ed9e626a37f7067b594422411539aa807190573", - "reference": "1ed9e626a37f7067b594422411539aa807190573", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/1aba28b697c1e3b6bbec8a1725f8b11b6d3e5a5f", + "reference": "1aba28b697c1e3b6bbec8a1725f8b11b6d3e5a5f", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.29" + "phpstan/phpstan": "^2.1.39" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -1557,11 +1557,14 @@ "MIT" ], "description": "Extra strict and opinionated rules for PHPStan", + "keywords": [ + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.8" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.10" }, - "time": "2026-01-27T08:10:25+00:00" + "time": "2026-02-11T14:17:32+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/docker/sdk b/docker/sdk index b689e14c..9cde8ec9 100755 --- a/docker/sdk +++ b/docker/sdk @@ -84,8 +84,14 @@ case $1 in docker compose stop ;; cli) - if [ -n "$2" ]; then - $DOCKER_EXEC php -f $2 + if [ -n "$5" ]; then + $DOCKER_EXEC php "$2" "$3" "$4" "$5" + elif [ -n "$4" ]; then + $DOCKER_EXEC php "$2" "$3" "$4" + elif [ -n "$3" ]; then + $DOCKER_EXEC php "$2" "$3" + elif [ -n "$2" ]; then + $DOCKER_EXEC php $2 else $DOCKER_EXEC bash fi diff --git a/src/Command/TransferGeneratorCommand.php b/src/Command/TransferGeneratorCommand.php index c6fad108..60877576 100644 --- a/src/Command/TransferGeneratorCommand.php +++ b/src/Command/TransferGeneratorCommand.php @@ -122,27 +122,22 @@ private function generateTransfers(SymfonyStyle $io, string $configPath): bool } $this->writelnErrorMessages($io, $generatorTransfer); - $this->writelnDebugMessages($io, $generatorTransfer); + if ($this->isDebugMessages($io, $generatorTransfer)) { + $lastFileName ??= $generatorTransfer->fileName; + $this->writelnDebugMessages($io, $generatorTransfer, $lastFileName); + } } return $generatorFiber->getReturn(); } - private function writelnDebugMessages(SymfonyStyle $io, TransferGeneratorTransfer $generatorTransfer): void - { - if ( - !$io->isVerbose() - || $generatorTransfer->validator->isValid === false - || $generatorTransfer->fileName === null - || $generatorTransfer->className === null - ) { - return; - } - - static $fileName = $generatorTransfer->fileName; - - if ($fileName !== $generatorTransfer->fileName) { - $fileName = $generatorTransfer->fileName; + private function writelnDebugMessages( + SymfonyStyle $io, + TransferGeneratorTransfer $generatorTransfer, + ?string &$lastFileName, + ): void { + if ($lastFileName !== $generatorTransfer->fileName) { + $lastFileName = $generatorTransfer->fileName; $io->newLine(); } @@ -156,6 +151,14 @@ private function writelnDebugMessages(SymfonyStyle $io, TransferGeneratorTransfe ); } + private function isDebugMessages(SymfonyStyle $io, TransferGeneratorTransfer $generatorTransfer): bool + { + return $io->isVerbose() + && $generatorTransfer->validator->isValid === true + && $generatorTransfer->fileName !== null + && $generatorTransfer->className !== null; + } + private function writelnErrorMessages(SymfonyStyle $io, TransferGeneratorTransfer $generatorTransfer): void { if ($generatorTransfer->validator->isValid === true) { diff --git a/src/DefinitionGenerator/Content/Expander/BuiltInTypeBuilderExpander.php b/src/DefinitionGenerator/Content/Expander/BuiltInTypeBuilderExpander.php index dfa0c63f..df846766 100644 --- a/src/DefinitionGenerator/Content/Expander/BuiltInTypeBuilderExpander.php +++ b/src/DefinitionGenerator/Content/Expander/BuiltInTypeBuilderExpander.php @@ -132,17 +132,13 @@ private function createDateTimePropertyTransfer(string $propertyName): Definitio private function createPropertyTransfer(string $propertyName, string $builtInType): DefinitionPropertyTransfer { - $tapeWithDocBlock = $this->parseTypeWithDocBlock($builtInType) ?? []; + $typeDocBlock = $this->parseTypeWithDocBlock($builtInType); - /** @var string $type */ - $type = array_key_first($tapeWithDocBlock); - $type = BuiltInTypeEnum::from($type); - - $docBlock = array_first($tapeWithDocBlock); + $type = BuiltInTypeEnum::from($typeDocBlock->type); $builtInTypeTransfer = new DefinitionBuiltInTypeTransfer(); $builtInTypeTransfer->name = $type; - $builtInTypeTransfer->docBlock = $docBlock; + $builtInTypeTransfer->docBlock = $typeDocBlock->docBlock; $propertyTransfer = new DefinitionPropertyTransfer(); $propertyTransfer->propertyName = $propertyName; diff --git a/src/Shared/Filesystem/FileAppender.php b/src/Shared/Filesystem/FileAppender.php index 9b65f13b..1aef156f 100644 --- a/src/Shared/Filesystem/FileAppender.php +++ b/src/Shared/Filesystem/FileAppender.php @@ -20,7 +20,7 @@ public function appendToFile(string $filename, string $content): void if ($writeResult === false) { throw new FileAppenderException( - sprintf('Failed to write content into the file "%s".', $filename), + sprintf('Failed to write content to file "%s".', $filename), ); } } @@ -35,7 +35,7 @@ public function closeFile(string $filename): void $isClosed = $this->fclose($file); if ($isClosed === false) { throw new FileAppenderException( - sprintf('Failed to close the file "%s".', $filename), + sprintf('Failed to close file "%s".', $filename), ); } diff --git a/src/Shared/Filesystem/FileReader.php b/src/Shared/Filesystem/FileReader.php index 12d72fce..9a7127d7 100644 --- a/src/Shared/Filesystem/FileReader.php +++ b/src/Shared/Filesystem/FileReader.php @@ -61,7 +61,7 @@ private function assertEndOfFile($file, string $filename, int $fileLine): void $this->fclose($file); throw new FileReaderException( - sprintf('Failed to read file "%s" line "%d".', $filename, $fileLine), + sprintf('Failed to read file "%s" at line "%d".', $filename, $fileLine), ); } diff --git a/src/Shared/Parser/DocBlockParserTrait.php b/src/Shared/Parser/DocBlockParserTrait.php index 34953e9b..010173f1 100644 --- a/src/Shared/Parser/DocBlockParserTrait.php +++ b/src/Shared/Parser/DocBlockParserTrait.php @@ -6,24 +6,17 @@ trait DocBlockParserTrait { - private const string TYPE_KEY = 'type'; + private const string DOCK_BLOCK_REGEX = '#^(?[^<> ]+)\s*(?<.+>)$#'; - private const string DOC_BLOCK_KEY = 'docBlock'; - - private const string TYPE_REGEX = '#(?[^<>]*)(?.*)#'; - - /** - * @return array|null - */ - final protected function parseTypeWithDocBlock(string $type): ?array + final protected function parseTypeWithDocBlock(string $type): TypeDocBlock { - if (preg_match(self::TYPE_REGEX, $type, $matches) === false) { - return null; + if (!str_contains($type, '<') || preg_match(self::DOCK_BLOCK_REGEX, $type, $matches) !== 1) { + return new TypeDocBlock(type: $type); } - $type = $matches[self::TYPE_KEY] ?? ''; - $docBlock = $matches[self::DOC_BLOCK_KEY] ?? null; - - return [$type => $docBlock]; + return new TypeDocBlock( + type: $matches['type'], + docBlock: $matches['docBlock'], + ); } } diff --git a/src/Shared/Parser/TypeDocBlock.php b/src/Shared/Parser/TypeDocBlock.php new file mode 100644 index 00000000..65b9076b --- /dev/null +++ b/src/Shared/Parser/TypeDocBlock.php @@ -0,0 +1,14 @@ + */ @@ -262,9 +264,8 @@ private function isBcMathType(mixed $type): bool private function isBcMathLoaded(): bool { - /** @var bool $isLoaded */ - static $isLoaded = \extension_loaded('bcmath'); + self::$_isBcMathLoaded ??= \extension_loaded('bcmath'); - return $isLoaded; + return self::$_isBcMathLoaded; } } diff --git a/src/TransferGenerator/Definition/Parser/Expander/AttributesPropertyExpander.php b/src/TransferGenerator/Definition/Parser/Expander/AttributesPropertyExpander.php index 20eeb248..1cbc30c6 100644 --- a/src/TransferGenerator/Definition/Parser/Expander/AttributesPropertyExpander.php +++ b/src/TransferGenerator/Definition/Parser/Expander/AttributesPropertyExpander.php @@ -14,7 +14,7 @@ final class AttributesPropertyExpander implements PropertyExpanderInterface private const string ATTRIBUTES_KEY = 'attributes'; - private const string ATTRIBUTES_REGEX = '#(?[^()]*)(?.*)#'; + private const string ATTRIBUTES_REGEX = '#^(?[^()]+)\s*(?.*)$#'; public function __construct( private readonly NamespaceBuilderInterface $namespaceBuilder, @@ -51,15 +51,14 @@ protected function handleExpander(array $matchedType, DefinitionPropertyTransfer private function getAttributeTransfer(string $attribute): ?DefinitionAttributeTransfer { - if (preg_match(self::ATTRIBUTES_REGEX, $attribute, $matches) === false) { + if (preg_match(self::ATTRIBUTES_REGEX, $attribute, $matches) !== 1) { return null; } - $namespace = $matches['namespace'] ?? ''; - $namespaceTransfer = $this->namespaceBuilder->createNamespaceTransfer($namespace); + $namespaceTransfer = $this->namespaceBuilder->createNamespaceTransfer($matches['namespace']); $builtInTypeTransfer = new DefinitionAttributeTransfer(); - $builtInTypeTransfer->arguments = $matches['arguments'] ?? null; + $builtInTypeTransfer->arguments = $matches['arguments']; $builtInTypeTransfer->namespace = $namespaceTransfer; return $builtInTypeTransfer; diff --git a/src/TransferGenerator/Definition/Parser/Expander/TypePropertyExpander.php b/src/TransferGenerator/Definition/Parser/Expander/TypePropertyExpander.php index 1eb14798..90d94817 100644 --- a/src/TransferGenerator/Definition/Parser/Expander/TypePropertyExpander.php +++ b/src/TransferGenerator/Definition/Parser/Expander/TypePropertyExpander.php @@ -14,6 +14,8 @@ final class TypePropertyExpander extends AbstractPropertyExpander { use DocBlockParserTrait; + private const string TYPE_KEY = 'type'; + public function __construct( private readonly EmbeddedTypeBuilderInterface $typeBuilder, ) { @@ -41,14 +43,8 @@ protected function handleExpander(string $matchedType, DefinitionPropertyTransfe private function getBuiltInTypeTransfer(string $matchedType): ?DefinitionBuiltInTypeTransfer { - $tapeWithDocBlock = $this->parseTypeWithDocBlock($matchedType); - if ($tapeWithDocBlock === null) { - return null; - } - - /** @var string $type */ - $type = array_key_first($tapeWithDocBlock); - $type = BuiltInTypeEnum::tryFrom($type); + $typeDocBlock = $this->parseTypeWithDocBlock($matchedType); + $type = BuiltInTypeEnum::tryFrom($typeDocBlock->type); if ($type === null) { return null; @@ -56,7 +52,7 @@ private function getBuiltInTypeTransfer(string $matchedType): ?DefinitionBuiltIn $builtInTypeTransfer = new DefinitionBuiltInTypeTransfer(); $builtInTypeTransfer->name = $type; - $builtInTypeTransfer->docBlock = array_first($tapeWithDocBlock); + $builtInTypeTransfer->docBlock = $typeDocBlock->docBlock; return $builtInTypeTransfer; } diff --git a/src/TransferGenerator/Definition/Validator/Content/Property/NumberTypePropertyValidator.php b/src/TransferGenerator/Definition/Validator/Content/Property/NumberTypePropertyValidator.php index b3865249..a4d72bcd 100644 --- a/src/TransferGenerator/Definition/Validator/Content/Property/NumberTypePropertyValidator.php +++ b/src/TransferGenerator/Definition/Validator/Content/Property/NumberTypePropertyValidator.php @@ -9,7 +9,7 @@ use Picamator\TransferObject\Generated\ValidatorMessageTransfer; use Picamator\TransferObject\Shared\Validator\ValidatorMessageTrait; -readonly class NumberTypePropertyValidator implements PropertyValidatorInterface +class NumberTypePropertyValidator implements PropertyValidatorInterface { use ValidatorMessageTrait; @@ -19,6 +19,8 @@ private const string EXTENSION_IS_NOT_LOADED_ERROR = 'PHP extension BCMath was not loaded. Please install and load extension.'; + private static bool $isBcMathLoaded; + public function isApplicable(DefinitionPropertyTransfer $propertyTransfer): bool { return $propertyTransfer->numberType !== null; @@ -57,9 +59,8 @@ private function getErrorMessage(DefinitionPropertyTransfer $propertyTransfer): protected function isBcMathLoaded(): bool { - /** @var bool $isLoaded */ - static $isLoaded = extension_loaded('bcmath'); + self::$isBcMathLoaded ??= extension_loaded('bcmath'); - return $isLoaded; + return self::$isBcMathLoaded; } } diff --git a/src/TransferGenerator/Generator/Render/RenderFactory.php b/src/TransferGenerator/Generator/Render/RenderFactory.php index 0f85ef62..0c113105 100644 --- a/src/TransferGenerator/Generator/Render/RenderFactory.php +++ b/src/TransferGenerator/Generator/Render/RenderFactory.php @@ -60,6 +60,7 @@ protected function createTemplateBuilder(): TemplateBuilderInterface { return new TemplateBuilder( $this->getConfig(), + $this->createTemplateSorter(), $this->createTemplateExpander(), ); } @@ -137,4 +138,9 @@ protected function createCollectionTypeTemplateExpander(): TemplateExpanderInter { return new CollectionTypeTemplateExpander(); } + + protected function createTemplateSorter(): TemplateSorterInterface + { + return new TemplateSorter(); + } } diff --git a/src/TransferGenerator/Generator/Render/TemplateBuilder.php b/src/TransferGenerator/Generator/Render/TemplateBuilder.php index 34bdf7be..0e9c545c 100644 --- a/src/TransferGenerator/Generator/Render/TemplateBuilder.php +++ b/src/TransferGenerator/Generator/Render/TemplateBuilder.php @@ -14,6 +14,7 @@ { public function __construct( private ConfigInterface $config, + private TemplateSorterInterface $templateSorter, private TemplateExpanderInterface $templateExpander, ) { } @@ -26,30 +27,19 @@ public function createTemplateTransfer(DefinitionTransfer $definitionTransfer): $templateTransfer->className = $definitionTransfer->content->className; $templateTransfer->imports[TransferEnum::ABSTRACT_CLASS->value] = TransferEnum::ABSTRACT_CLASS->value; - foreach ($definitionTransfer->content->properties as $propertyTransfer) { - $this->templateExpander->expandTemplateTransfer($propertyTransfer, $templateTransfer); - } + $this->expandProperties($definitionTransfer, $templateTransfer); - $this->sortTemplate($templateTransfer); + $this->templateSorter->sortTemplateTransfer($templateTransfer); return $templateTransfer; } - private function sortTemplate(TemplateTransfer $templateTransfer): void - { - $templateTransfer->imports->natsort(); - $templateTransfer->metaConstants->natsort(); - - if ($templateTransfer->metaInitiators->count() > 0) { - $templateTransfer->metaInitiators->natsort(); - } - - if ($templateTransfer->metaTransformers->count() > 0) { - $templateTransfer->metaTransformers->natsort(); - } - - foreach ($templateTransfer->metaAttributes as $metaAttributes) { - natsort($metaAttributes); + private function expandProperties( + DefinitionTransfer $definitionTransfer, + TemplateTransfer $templateTransfer, + ): void { + foreach ($definitionTransfer->content->properties as $propertyTransfer) { + $this->templateExpander->expandTemplateTransfer($propertyTransfer, $templateTransfer); } } diff --git a/src/TransferGenerator/Generator/Render/TemplateSorter.php b/src/TransferGenerator/Generator/Render/TemplateSorter.php new file mode 100644 index 00000000..b5c32087 --- /dev/null +++ b/src/TransferGenerator/Generator/Render/TemplateSorter.php @@ -0,0 +1,42 @@ +imports->uasort($this->sortNamespaces(...)); + $templateTransfer->metaConstants->natsort(); + + if ($templateTransfer->metaInitiators->count() > 0) { + $templateTransfer->metaInitiators->natsort(); + } + + if ($templateTransfer->metaTransformers->count() > 0) { + $templateTransfer->metaTransformers->natsort(); + } + + $this->sortMetaAttributes($templateTransfer); + } + + private function sortMetaAttributes(TemplateTransfer $templateTransfer): void + { + foreach ($templateTransfer->metaAttributes as &$metaAttributes) { + natsort($metaAttributes); + } + unset($metaAttributes); + } + + private function sortNamespaces(string $a, string $b): int + { + return strcasecmp( + str_replace(search: '\\', replace: '', subject: $a), + str_replace(search: '\\', replace: '', subject: $b), + ); + } +} diff --git a/src/TransferGenerator/Generator/Render/TemplateSorterInterface.php b/src/TransferGenerator/Generator/Render/TemplateSorterInterface.php new file mode 100644 index 00000000..d585f166 --- /dev/null +++ b/src/TransferGenerator/Generator/Render/TemplateSorterInterface.php @@ -0,0 +1,12 @@ + self::RUN_INDEX, + ]; + + // run + public const string RUN_PROP = 'run'; + private const int RUN_INDEX = 0; + + public ?true $run { + get => $this->getData(self::RUN_INDEX); + set { + $this->setData(self::RUN_INDEX, $value); + } + } +} diff --git a/tests/integration/Command/TransferGeneratorCommandTest.php b/tests/integration/Command/TransferGeneratorCommandTest.php index db05939c..ab21f75c 100644 --- a/tests/integration/Command/TransferGeneratorCommandTest.php +++ b/tests/integration/Command/TransferGeneratorCommandTest.php @@ -65,7 +65,8 @@ public function testRunCommandWithValidConfigurationShouldShowSuccessMessage(): // Assert $this->commandTester->assertCommandIsSuccessful(); - $this->assertStringContainsString('command.transfer.yml: CommandTransfer', $output); + $this->assertStringContainsString('command.first.transfer.yml: CommandFirstTransfer', $output); + $this->assertStringContainsString('command.second.transfer.yml: CommandSecondTransfer', $output); $this->assertStringContainsString('All Transfer Objects were generated successfully!', $output); } diff --git a/tests/integration/Command/data/config/success/definition/command.transfer.yml b/tests/integration/Command/data/config/success/definition/command.first.transfer.yml similarity index 86% rename from tests/integration/Command/data/config/success/definition/command.transfer.yml rename to tests/integration/Command/data/config/success/definition/command.first.transfer.yml index 656e48bd..4c2cf5f4 100644 --- a/tests/integration/Command/data/config/success/definition/command.transfer.yml +++ b/tests/integration/Command/data/config/success/definition/command.first.transfer.yml @@ -1,4 +1,4 @@ # $schema: ./../../../../../../../schema/definition.schema.json -Command: +CommandFirst: run: type: true diff --git a/tests/integration/Command/data/config/success/definition/command.second.transfer.yml b/tests/integration/Command/data/config/success/definition/command.second.transfer.yml new file mode 100644 index 00000000..9e67b7b9 --- /dev/null +++ b/tests/integration/Command/data/config/success/definition/command.second.transfer.yml @@ -0,0 +1,4 @@ +# $schema: ./../../../../../../../schema/definition.schema.json +CommandSecond: + run: + type: true diff --git a/tests/integration/DefinitionGenerator/DefinitionGeneratorFacadeTest.php b/tests/integration/DefinitionGenerator/DefinitionGeneratorFacadeTest.php index 353ce155..bf629c1f 100644 --- a/tests/integration/DefinitionGenerator/DefinitionGeneratorFacadeTest.php +++ b/tests/integration/DefinitionGenerator/DefinitionGeneratorFacadeTest.php @@ -17,6 +17,7 @@ use Picamator\Tests\Integration\TransferObject\DefinitionGenerator\Generated\NasaNeo\AsteroidTransfer; use Picamator\Tests\Integration\TransferObject\DefinitionGenerator\Generated\OpenWeather\ForecastTransfer; use Picamator\Tests\Integration\TransferObject\DefinitionGenerator\Generated\Tagesschau\ArdNewsTransfer; +use Picamator\Tests\Integration\TransferObject\DefinitionGenerator\Generated\Wero\PaymentChargesTransfer; use Picamator\Tests\Integration\TransferObject\Helper\DefinitionGeneratorTrait; use Picamator\Tests\Integration\TransferObject\Helper\FilterArrayTrait; use Picamator\Tests\Integration\TransferObject\Helper\TransferGeneratorTrait; @@ -113,6 +114,12 @@ public static function generateDefinitionDataProvider(): Generator 'sampleFileName' => 'genesis-destatis-find.json', 'definitionFileName' => 'destatis.transfer.yml', ]; + + yield 'Wero' => [ + 'className' => 'PaymentCharges', + 'sampleFileName' => 'wero-payment-charges-v1.json', + 'definitionFileName' => 'paymentCharges.transfer.yml', + ]; } #[DataProvider('configPathDataProvider')] @@ -160,6 +167,10 @@ public static function configPathDataProvider(): Generator yield 'Destatis' => [ 'genesis-destatis-find.json', ]; + + yield 'Wero' => [ + 'wero-payment-charges-v1.json', + ]; } #[DataProvider('matchDefinitionDataProvider')] @@ -215,6 +226,11 @@ public static function matchDefinitionDataProvider(): Generator DestatisTransfer::class, 'genesis-destatis-find.json', ]; + + yield 'Wero' => [ + PaymentChargesTransfer::class, + 'wero-payment-charges-v1.json', + ]; } #[DataProvider('matchFilteredDefinitionDataProvider')] diff --git a/tests/integration/DefinitionGenerator/Generated/Wero/AmountTransfer.php b/tests/integration/DefinitionGenerator/Generated/Wero/AmountTransfer.php new file mode 100644 index 00000000..d5d50ace --- /dev/null +++ b/tests/integration/DefinitionGenerator/Generated/Wero/AmountTransfer.php @@ -0,0 +1,48 @@ + self::CURRENCY_INDEX, + self::VALUE_PROP => self::VALUE_INDEX, + ]; + + // currency + public const string CURRENCY_PROP = 'currency'; + private const int CURRENCY_INDEX = 0; + + public ?string $currency { + get => $this->getData(self::CURRENCY_INDEX); + set { + $this->setData(self::CURRENCY_INDEX, $value); + } + } + + // value + public const string VALUE_PROP = 'value'; + private const int VALUE_INDEX = 1; + + public ?int $value { + get => $this->getData(self::VALUE_INDEX); + set { + $this->setData(self::VALUE_INDEX, $value); + } + } +} diff --git a/tests/integration/DefinitionGenerator/Generated/Wero/AuthenticationSettingsTransfer.php b/tests/integration/DefinitionGenerator/Generated/Wero/AuthenticationSettingsTransfer.php new file mode 100644 index 00000000..48683f02 --- /dev/null +++ b/tests/integration/DefinitionGenerator/Generated/Wero/AuthenticationSettingsTransfer.php @@ -0,0 +1,54 @@ + self::SETTINGS_INDEX, + self::TYPE_PROP => self::TYPE_INDEX, + ]; + + protected const array META_TRANSFORMERS = [ + self::SETTINGS_PROP => 'SETTINGS_PROP', + ]; + + // settings + #[TransferTransformerAttribute(SettingsTransfer::class)] + public const string SETTINGS_PROP = 'settings'; + private const int SETTINGS_INDEX = 0; + + public ?SettingsTransfer $settings { + get => $this->getData(self::SETTINGS_INDEX); + set { + $this->setData(self::SETTINGS_INDEX, $value); + } + } + + // type + public const string TYPE_PROP = 'type'; + private const int TYPE_INDEX = 1; + + public ?string $type { + get => $this->getData(self::TYPE_INDEX); + set { + $this->setData(self::TYPE_INDEX, $value); + } + } +} diff --git a/tests/integration/DefinitionGenerator/Generated/Wero/ConsumerTransfer.php b/tests/integration/DefinitionGenerator/Generated/Wero/ConsumerTransfer.php new file mode 100644 index 00000000..86949006 --- /dev/null +++ b/tests/integration/DefinitionGenerator/Generated/Wero/ConsumerTransfer.php @@ -0,0 +1,36 @@ + self::COUNTRY_INDEX, + ]; + + // country + public const string COUNTRY_PROP = 'country'; + private const int COUNTRY_INDEX = 0; + + public ?string $country { + get => $this->getData(self::COUNTRY_INDEX); + set { + $this->setData(self::COUNTRY_INDEX, $value); + } + } +} diff --git a/tests/integration/DefinitionGenerator/Generated/Wero/PaymentChargesTransfer.php b/tests/integration/DefinitionGenerator/Generated/Wero/PaymentChargesTransfer.php new file mode 100644 index 00000000..3cf15dc7 --- /dev/null +++ b/tests/integration/DefinitionGenerator/Generated/Wero/PaymentChargesTransfer.php @@ -0,0 +1,91 @@ + self::AMOUNT_INDEX, + self::AUTHENTICATION_SETTINGS_PROP => self::AUTHENTICATION_SETTINGS_INDEX, + self::CONSUMER_PROP => self::CONSUMER_INDEX, + self::PAYMENT_METHOD_PROP => self::PAYMENT_METHOD_INDEX, + ]; + + protected const array META_INITIATORS = [ + self::AUTHENTICATION_SETTINGS_PROP => 'AUTHENTICATION_SETTINGS_PROP', + ]; + + protected const array META_TRANSFORMERS = [ + self::AMOUNT_PROP => 'AMOUNT_PROP', + self::AUTHENTICATION_SETTINGS_PROP => 'AUTHENTICATION_SETTINGS_PROP', + self::CONSUMER_PROP => 'CONSUMER_PROP', + ]; + + // amount + #[TransferTransformerAttribute(AmountTransfer::class)] + public const string AMOUNT_PROP = 'amount'; + private const int AMOUNT_INDEX = 0; + + public ?AmountTransfer $amount { + get => $this->getData(self::AMOUNT_INDEX); + set { + $this->setData(self::AMOUNT_INDEX, $value); + } + } + + // authenticationSettings + #[ArrayObjectInitiatorAttribute] + #[CollectionTransformerAttribute(AuthenticationSettingsTransfer::class)] + public const string AUTHENTICATION_SETTINGS_PROP = 'authenticationSettings'; + private const int AUTHENTICATION_SETTINGS_INDEX = 1; + + /** @var \ArrayObject */ + public ArrayObject $authenticationSettings { + get => $this->getData(self::AUTHENTICATION_SETTINGS_INDEX); + set { + $this->setData(self::AUTHENTICATION_SETTINGS_INDEX, $value); + } + } + + // consumer + #[TransferTransformerAttribute(ConsumerTransfer::class)] + public const string CONSUMER_PROP = 'consumer'; + private const int CONSUMER_INDEX = 2; + + public ?ConsumerTransfer $consumer { + get => $this->getData(self::CONSUMER_INDEX); + set { + $this->setData(self::CONSUMER_INDEX, $value); + } + } + + // paymentMethod + public const string PAYMENT_METHOD_PROP = 'paymentMethod'; + private const int PAYMENT_METHOD_INDEX = 3; + + public ?string $paymentMethod { + get => $this->getData(self::PAYMENT_METHOD_INDEX); + set { + $this->setData(self::PAYMENT_METHOD_INDEX, $value); + } + } +} diff --git a/tests/integration/DefinitionGenerator/Generated/Wero/SettingsTransfer.php b/tests/integration/DefinitionGenerator/Generated/Wero/SettingsTransfer.php new file mode 100644 index 00000000..910195fa --- /dev/null +++ b/tests/integration/DefinitionGenerator/Generated/Wero/SettingsTransfer.php @@ -0,0 +1,36 @@ + self::RETURN_URL_INDEX, + ]; + + // returnUrl + public const string RETURN_URL_PROP = 'returnUrl'; + private const int RETURN_URL_INDEX = 0; + + public ?string $returnUrl { + get => $this->getData(self::RETURN_URL_INDEX); + set { + $this->setData(self::RETURN_URL_INDEX, $value); + } + } +} diff --git a/tests/integration/DefinitionGenerator/data/README.md b/tests/integration/DefinitionGenerator/data/README.md index 613d4731..fef3b8d6 100644 --- a/tests/integration/DefinitionGenerator/data/README.md +++ b/tests/integration/DefinitionGenerator/data/README.md @@ -4,8 +4,9 @@ API Response Reference | File | Source | |---------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| | [nasa-neo-rest-v1-neo-2465633.json](/tests/integration/DefinitionGenerator/data/api-response/nasa-neo-rest-v1-neo-2465633.json) | [NASA Open Api](https://api.nasa.gov/neo/rest/v1/neo/2465633?api_key=DEMO_KEY) | -| [open-weather.json](/tests/integration/DefinitionGenerator/data/api-response/open-weather.json) | [OpenWeather](https://openweathermap.org/current#example_JSON) | -| [google-shopping-content.json](/tests/integration/DefinitionGenerator/data/api-response/google-shopping-content.json) | [Content API for Shopping](https://developers.google.com/shopping-content/guides/products/products-api?hl=en) | -| [frankfurter-dev.json](/tests/integration/DefinitionGenerator/data/api-response/frankfurter-dev-v1.json) | [Frankfurter is a free, open-source currency data API](https://api.frankfurter.dev/v1/latest) | +| [open-weather.json](/tests/integration/DefinitionGenerator/data/api-response/open-weather.json) | [OpenWeather](https://openweathermap.org/current?collection=current_forecast#example_JSON) | +| [google-shopping-content.json](/tests/integration/DefinitionGenerator/data/api-response/google-shopping-content.json) | [Google Content API for Shopping](https://developers.google.com/shopping-content/guides/products/products-api?hl=en) | +| [frankfurter-dev.json](/tests/integration/DefinitionGenerator/data/api-response/frankfurter-dev-v1.json) | [Frankfurter - open-source currency data API](https://api.frankfurter.dev/v1/latest) | | [tagesschau-api-bund-dev.json](/tests/integration/DefinitionGenerator/data/api-response/tagesschau-api-bund-dev-v2.json) | [Tagesschau API](https://tagesschau.api.bund.dev) | | [genesis-destatis-find.json](/tests/integration/DefinitionGenerator/data/api-response/genesis-destatis-find.json) | [Statistisches Bundesamt (Destatis)](https://www-genesis.destatis.de/genesisWS/swagger-ui/index.html#/find/findPost) | +| [wero-payment-charges-v1.json](/tests/integration/DefinitionGenerator/data/api-response/wero-payment-charges-v1.json) | [Wero - Digital Payment Wallet](https://developerhub.ppro.com/global-api/docs/wero) | diff --git a/tests/integration/DefinitionGenerator/data/api-response/open-weather.json b/tests/integration/DefinitionGenerator/data/api-response/open-weather.json index 6c9d348f..13e7f17b 100644 --- a/tests/integration/DefinitionGenerator/data/api-response/open-weather.json +++ b/tests/integration/DefinitionGenerator/data/api-response/open-weather.json @@ -1,49 +1,49 @@ { - "coord": { - "lon": 7.367, - "lat": 45.133 - }, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "base": "stations", - "main": { - "temp": 284.2, - "feels_like": 282.93, - "temp_min": 283.06, - "temp_max": 286.82, - "pressure": 1021, - "humidity": 60, - "sea_level": 1021, - "grnd_level": 910 - }, - "visibility": 10000, - "wind": { - "speed": 4.09, - "deg": 121, - "gust": 3.47 - }, - "rain": { - "1h": 2.73 - }, - "clouds": { - "all": 83 - }, - "dt": 1726660758, - "sys": { - "type": 1, - "id": 6736, - "country": "IT", - "sunrise": 1726636384, - "sunset": 1726680975 - }, - "timezone": 7200, - "id": 3165523, - "name": "Province of Turin", - "cod": 200 + "coord": { + "lon": 7.367, + "lat": 45.133 + }, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "base": "stations", + "main": { + "temp": 284.2, + "feels_like": 282.93, + "temp_min": 283.06, + "temp_max": 286.82, + "pressure": 1021, + "humidity": 60, + "sea_level": 1021, + "grnd_level": 910 + }, + "visibility": 10000, + "wind": { + "speed": 4.09, + "deg": 121, + "gust": 3.47 + }, + "rain": { + "1h": 2.73 + }, + "clouds": { + "all": 83 + }, + "dt": 1726660758, + "sys": { + "type": 1, + "id": 6736, + "country": "IT", + "sunrise": 1726636384, + "sunset": 1726680975 + }, + "timezone": 7200, + "id": 3165523, + "name": "Province of Turin", + "cod": 200 } diff --git a/tests/integration/DefinitionGenerator/data/api-response/wero-payment-charges-v1.json b/tests/integration/DefinitionGenerator/data/api-response/wero-payment-charges-v1.json new file mode 100644 index 00000000..2808c629 --- /dev/null +++ b/tests/integration/DefinitionGenerator/data/api-response/wero-payment-charges-v1.json @@ -0,0 +1,18 @@ +{ + "paymentMethod": "WERO", + "amount": { + "value": 1000, + "currency": "EUR" + }, + "consumer": { + "country": "DE" + }, + "authenticationSettings": [ + { + "type": "REDIRECT", + "settings": { + "returnUrl": "https://www.webshop.com/order-results-page" + } + } + ] +} diff --git a/tests/integration/DefinitionGenerator/data/config/wero-payment-charges-v1/definition/paymentCharges.transfer.yml b/tests/integration/DefinitionGenerator/data/config/wero-payment-charges-v1/definition/paymentCharges.transfer.yml new file mode 100644 index 00000000..eecbe227 --- /dev/null +++ b/tests/integration/DefinitionGenerator/data/config/wero-payment-charges-v1/definition/paymentCharges.transfer.yml @@ -0,0 +1,37 @@ +# $schema: https://raw.githubusercontent.com/picamator/transfer-object/main/schema/definition.schema.json + +# PaymentCharges +PaymentCharges: + paymentMethod: + type: string + amount: + type: Amount + consumer: + type: Consumer + authenticationSettings: + collectionType: AuthenticationSettings + +# Amount +Amount: + value: + type: int + currency: + type: string + +# Consumer +Consumer: + country: + type: string + +# AuthenticationSettings +AuthenticationSettings: + type: + type: string + settings: + type: Settings + +# Settings +Settings: + returnUrl: + type: string + diff --git a/tests/integration/DefinitionGenerator/data/config/wero-payment-charges-v1/generator.config.yml b/tests/integration/DefinitionGenerator/data/config/wero-payment-charges-v1/generator.config.yml new file mode 100644 index 00000000..a3b75551 --- /dev/null +++ b/tests/integration/DefinitionGenerator/data/config/wero-payment-charges-v1/generator.config.yml @@ -0,0 +1,5 @@ +# $schema: ./../../../../../../schema/config.schema.json +generator: + transferNamespace: "Picamator\\Tests\\Integration\\TransferObject\\DefinitionGenerator\\Generated\\Wero" + transferPath: "${PROJECT_ROOT}/tests/integration/DefinitionGenerator/Generated/Wero" + definitionPath: "${PROJECT_ROOT}/tests/integration/DefinitionGenerator/data/config/wero-payment-charges-v1/definition" diff --git a/tests/integration/TransferGenerator/Generated/Error/AddressStatisticsTransfer.php b/tests/integration/TransferGenerator/Generated/Error/AddressStatisticsTransfer.php index 8cbb508b..98e746f4 100644 --- a/tests/integration/TransferGenerator/Generated/Error/AddressStatisticsTransfer.php +++ b/tests/integration/TransferGenerator/Generated/Error/AddressStatisticsTransfer.php @@ -5,6 +5,7 @@ namespace Picamator\Tests\Integration\TransferObject\TransferGenerator\Generated\Error; use Picamator\TransferObject\Transfer\AbstractTransfer; +use Picamator\TransferObject\Transfer\Attribute\Transformer\TransferTransformerAttribute; /** * Specification: @@ -13,22 +14,43 @@ * * Note: Do not manually edit this file, as changes will be overwritten. * - * @see /tests/integration/TransferGenerator/data/config/error/invalid-attribute-name/definition/address-statistics.transfer.yml Definition file path. + * @see /tests/integration/TransferGenerator/data/config/error/unsupported-type/definition/address-statistics.transfer.yml Definition file path. */ final class AddressStatisticsTransfer extends AbstractTransfer { - protected const int META_DATA_SIZE = 1; + protected const int META_DATA_SIZE = 2; protected const array META_DATA = [ - self::ADDRESS_BOOK_UUID_INDEX => self::ADDRESS_BOOK_UUID, + self::ADDRESS_BOOK_UUID_PROP => self::ADDRESS_BOOK_UUID_INDEX, + self::ADDRESS_UUID_PROP => self::ADDRESS_UUID_INDEX, + ]; + + protected const array META_TRANSFORMERS = [ + self::ADDRESS_BOOK_UUID_PROP => 'ADDRESS_BOOK_UUID_PROP', + self::ADDRESS_UUID_PROP => 'ADDRESS_UUID_PROP', ]; // addressBookUuid - public const string ADDRESS_BOOK_UUID = 'addressBookUuid'; + #[TransferTransformerAttribute(objectTransfer::class)] + public const string ADDRESS_BOOK_UUID_PROP = 'addressBookUuid'; private const int ADDRESS_BOOK_UUID_INDEX = 0; - public ?string $addressBookUuid { + public ?objectTransfer $addressBookUuid { get => $this->getData(self::ADDRESS_BOOK_UUID_INDEX); - set => $this->setData(self::ADDRESS_BOOK_UUID_INDEX, $value); + set { + $this->setData(self::ADDRESS_BOOK_UUID_INDEX, $value); + } + } + + // addressUuid + #[TransferTransformerAttribute(stringTransfer::class)] + public const string ADDRESS_UUID_PROP = 'addressUuid'; + private const int ADDRESS_UUID_INDEX = 1; + + public ?stringTransfer $addressUuid { + get => $this->getData(self::ADDRESS_UUID_INDEX); + set { + $this->setData(self::ADDRESS_UUID_INDEX, $value); + } } } diff --git a/tests/unit/DefinitionGenerator/Content/Expander/BuiltInTypeBuilderExpanderTest.php b/tests/unit/DefinitionGenerator/Content/Expander/BuiltInTypeBuilderExpanderTest.php index 6cce44a0..ee49cdce 100644 --- a/tests/unit/DefinitionGenerator/Content/Expander/BuiltInTypeBuilderExpanderTest.php +++ b/tests/unit/DefinitionGenerator/Content/Expander/BuiltInTypeBuilderExpanderTest.php @@ -4,6 +4,7 @@ namespace Picamator\Tests\Unit\TransferObject\DefinitionGenerator\Content\Expander; +use ArrayObject; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -13,6 +14,8 @@ use Picamator\TransferObject\DefinitionGenerator\Content\Expander\BuiltInTypeBuilderExpander; use Picamator\TransferObject\DefinitionGenerator\Exception\DefinitionGeneratorException; use Picamator\TransferObject\Generated\DefinitionBuilderTransfer; +use Picamator\TransferObject\Generated\DefinitionContentTransfer; +use Picamator\TransferObject\TransferGenerator\Definition\Enum\BuiltInTypeEnum; use stdClass; #[Group('definition-generator')] @@ -43,4 +46,30 @@ public function testUnsupportedTypeShouldThrowException(): void // Act $this->expander->expandBuilderTransfer($content, $builderTransfer); } + + #[TestDox('Array Object Type')] + public function testArrayObjectType(): void + { + // Arrange + $content = new Content( + type: GetTypeEnum::object, + propertyName: 'someArrayObject', + propertyValue: new ArrayObject(['test' => 1]), + ); + + $builderTransfer = new DefinitionBuilderTransfer(); + $builderTransfer->definitionContent = new DefinitionContentTransfer(); + + // Act + $this->expander->expandBuilderTransfer($content, $builderTransfer); + + // Assert + $this->assertCount(1, $builderTransfer->definitionContent->properties); + + /** @var \Picamator\TransferObject\Generated\DefinitionPropertyTransfer $propertyTransfer */ + $propertyTransfer = $builderTransfer->definitionContent->properties[0]; + $this->assertSame($content->propertyName, $propertyTransfer->propertyName); + $this->assertSame(BuiltInTypeEnum::ARRAY_OBJECT, $propertyTransfer->builtInType?->name); + $this->assertNull($propertyTransfer->builtInType->docBlock); + } } diff --git a/tests/unit/Shared/Parser/DocBlockParserInterface.php b/tests/unit/Shared/Parser/DocBlockParserInterface.php new file mode 100644 index 00000000..5e4cd2d4 --- /dev/null +++ b/tests/unit/Shared/Parser/DocBlockParserInterface.php @@ -0,0 +1,12 @@ +docBlockParser = new class () implements DocBlockParserInterface { + use DocBlockParserTrait { + parseTypeWithDocBlock as public; + } + }; + } + + #[TestWith(['ArrayObject', 'ArrayObject', null])] + #[TestWith(['ArrayObject', 'ArrayObject', ''])] + #[TestWith(['ArrayObject ', 'ArrayObject', ''])] + #[TestDox('Parse type "$type" expected type "$expectedType" and docBlock "$expectedDocBlock"')] + public function testParseTypeWithDocBlock(string $type, string $expectedType, ?string $expectedDocBlock): void + { + // Act + $actual = $this->docBlockParser->parseTypeWithDocBlock($type); + + // Assert + $this->assertSame($expectedType, $actual->type); + $this->assertSame($expectedDocBlock, $actual->docBlock); + } +}