From a82feeb415c45eb8cf4af92074b9c518137a0c1a Mon Sep 17 00:00:00 2001 From: Walmir Silva Date: Mon, 28 Oct 2024 18:46:14 -0300 Subject: [PATCH] feat(transformer): implement data transformation system and test coverage BREAKING CHANGE: Complete transformer system implementation with comprehensive test coverage feat: implement core transformation processors - Add DateTransformer with timezone and format support - Add ArrayTransformers (Flatten, Group, Key, Map) - Add StringTransformers (Case, Mask, Slug, Template) - Implement JsonTransformer and NumberTransformer refactor: enhance core components and traits - Optimize ArrayTransformerTrait for better performance - Improve StringTransformerTrait with enhanced validation - Strengthen type-safety across transformers - Implement strict type declarations test: add comprehensive test suites - Core system tests (AbstractProcessor, Base Transformer) - Array processing tests (Flatten, Group, Key, Map) - String processing tests (Case, Mask, Slug, Template) - Data handling tests (Date, JSON, Number) - Composite operation tests (Chain, Conditional) - Trait implementation tests fix: improve error handling - Add specialized DateTransformerException - Enhance validation mechanisms - Implement proper error messaging - Add type validation safeguards docs: update documentation - Add PHPDoc blocks - Include usage examples - Document error scenarios - Add type declarations --- .gitignore | 2 +- src/Exception/DateTransformerException.php | 41 +++ .../Array/ArrayFlattenTransformer.php | 8 +- src/Processor/Array/ArrayGroupTransformer.php | 3 - src/Processor/Array/ArrayKeyTransformer.php | 41 +-- src/Processor/Array/ArrayMapTransformer.php | 13 +- src/Processor/Data/DateTransformer.php | 90 ++++-- src/Processor/String/MaskTransformer.php | 115 +++++-- src/Trait/ArrayTransformerTrait.php | 36 +-- src/Trait/StringTransformerTrait.php | 15 +- src/Transformer.php | 49 --- tests/Exception/TransformerExceptionTest.php | 156 +++++++++ .../AbstractTransformerProcessorTest.php | 192 +++++++++++ .../Array/ArrayFlattenTransformerTest.php | 73 +++++ .../Array/ArrayGroupTransformerTest.php | 109 +++++++ .../Array/ArrayKeyTransformerTest.php | 67 ++++ .../Array/ArrayMapTransformerTest.php | 96 ++++++ .../Composite/ChainTransformerTest.php | 269 ++++++++++++++++ .../Composite/ConditionalTransformerTest.php | 121 +++++++ tests/Processor/Data/DateTransformerTest.php | 235 ++++++++++++++ tests/Processor/Data/JsonTransformerTest.php | 96 ++++++ .../Processor/Data/NumberTransformerTest.php | 72 +++++ .../Processor/String/CaseTransformerTest.php | 91 ++++++ .../Processor/String/MaskTransformerTest.php | 90 ++++++ .../Processor/String/SlugTransformerTest.php | 72 +++++ .../String/TemplateTransformerTest.php | 103 ++++++ tests/Result/TransformationResultTest.php | 80 +++++ tests/Trait/ArrayTransformerTraitTest.php | 81 +++++ tests/Trait/StringTransformerTraitTest.php | 304 ++++++++++++++++++ tests/TransformerTest.php | 75 +++++ 30 files changed, 2626 insertions(+), 169 deletions(-) create mode 100644 src/Exception/DateTransformerException.php create mode 100644 tests/Exception/TransformerExceptionTest.php create mode 100644 tests/Processor/AbstractTransformerProcessorTest.php create mode 100644 tests/Processor/Array/ArrayFlattenTransformerTest.php create mode 100644 tests/Processor/Array/ArrayGroupTransformerTest.php create mode 100644 tests/Processor/Array/ArrayKeyTransformerTest.php create mode 100644 tests/Processor/Array/ArrayMapTransformerTest.php create mode 100644 tests/Processor/Composite/ChainTransformerTest.php create mode 100644 tests/Processor/Composite/ConditionalTransformerTest.php create mode 100644 tests/Processor/Data/DateTransformerTest.php create mode 100644 tests/Processor/Data/JsonTransformerTest.php create mode 100644 tests/Processor/Data/NumberTransformerTest.php create mode 100644 tests/Processor/String/CaseTransformerTest.php create mode 100644 tests/Processor/String/MaskTransformerTest.php create mode 100644 tests/Processor/String/SlugTransformerTest.php create mode 100644 tests/Processor/String/TemplateTransformerTest.php create mode 100644 tests/Result/TransformationResultTest.php create mode 100644 tests/Trait/ArrayTransformerTraitTest.php create mode 100644 tests/Trait/StringTransformerTraitTest.php create mode 100644 tests/TransformerTest.php diff --git a/.gitignore b/.gitignore index 10edc2a..d7d85e4 100644 --- a/.gitignore +++ b/.gitignore @@ -63,5 +63,5 @@ tests/lista_de_arquivos.php tests/lista_de_arquivos_test.php lista_de_arquivos.txt lista_de_arquivos_tests.txt -add_static_to_providers.php +test_files_generate.php /composer.lock \ No newline at end of file diff --git a/src/Exception/DateTransformerException.php b/src/Exception/DateTransformerException.php new file mode 100644 index 0000000..b42552a --- /dev/null +++ b/src/Exception/DateTransformerException.php @@ -0,0 +1,41 @@ +separator . $key : $key; if (is_array($value) && ($depth > 0 || -1 === $depth)) { - $result = array_merge( - $result, - $this->flattenArray($value, $newKey, $depth > 0 ? $depth - 1 : -1) - ); + $result += $this->flattenArray($value, $newKey, $depth > 0 ? $depth - 1 : -1); } else { $result[$newKey] = $value; } diff --git a/src/Processor/Array/ArrayGroupTransformer.php b/src/Processor/Array/ArrayGroupTransformer.php index 58f404b..0246b16 100644 --- a/src/Processor/Array/ArrayGroupTransformer.php +++ b/src/Processor/Array/ArrayGroupTransformer.php @@ -6,12 +6,9 @@ use KaririCode\Contract\Processor\ConfigurableProcessor; use KaririCode\Transformer\Processor\AbstractTransformerProcessor; -use KaririCode\Transformer\Trait\ArrayTransformerTrait; class ArrayGroupTransformer extends AbstractTransformerProcessor implements ConfigurableProcessor { - use ArrayTransformerTrait; - private string $groupBy = ''; private bool $preserveKeys = false; diff --git a/src/Processor/Array/ArrayKeyTransformer.php b/src/Processor/Array/ArrayKeyTransformer.php index 7c1b7cf..3e422b3 100644 --- a/src/Processor/Array/ArrayKeyTransformer.php +++ b/src/Processor/Array/ArrayKeyTransformer.php @@ -12,12 +12,7 @@ class ArrayKeyTransformer extends AbstractTransformerProcessor implements Config { use ArrayTransformerTrait; - private const CASE_SNAKE = 'snake'; - private const CASE_CAMEL = 'camel'; - private const CASE_PASCAL = 'pascal'; - private const CASE_KEBAB = 'kebab'; - - private string $case = self::CASE_SNAKE; + private string $case = 'snake'; // Valores possíveis: snake, camel, pascal, kebab private bool $recursive = true; public function configure(array $options): void @@ -37,44 +32,26 @@ public function process(mixed $input): array return []; } - return $this->transformArrayKeys($input); + // Transforma as chaves apenas no nível principal se recursive for false + return $this->recursive + ? $this->transformArrayKeys($input, $this->case) + : $this->transformKeysNonRecursive($input, $this->case); } - private function transformArrayKeys(array $array): array + private function transformKeysNonRecursive(array $array, string $case): array { $result = []; foreach ($array as $key => $value) { - $transformedKey = $this->transformKey((string) $key); - - if (is_array($value) && $this->recursive) { - $result[$transformedKey] = $this->transformArrayKeys($value); - } else { - $result[$transformedKey] = $value; - } + $transformedKey = $this->transformKeyByCase((string) $key, $case); + $result[$transformedKey] = $value; // Mantém o valor original, sem recursão } return $result; } - private function transformKey(string $key): string - { - return match ($this->case) { - self::CASE_SNAKE => $this->toSnakeCase($key), - self::CASE_CAMEL => $this->toCamelCase($key), - self::CASE_PASCAL => $this->toPascalCase($key), - self::CASE_KEBAB => $this->toKebabCase($key), - default => $key, - }; - } - private function getAllowedCases(): array { - return [ - self::CASE_SNAKE, - self::CASE_CAMEL, - self::CASE_PASCAL, - self::CASE_KEBAB, - ]; + return ['snake', 'camel', 'pascal', 'kebab']; } } diff --git a/src/Processor/Array/ArrayMapTransformer.php b/src/Processor/Array/ArrayMapTransformer.php index fa4625d..e0c8cd9 100644 --- a/src/Processor/Array/ArrayMapTransformer.php +++ b/src/Processor/Array/ArrayMapTransformer.php @@ -15,6 +15,7 @@ class ArrayMapTransformer extends AbstractTransformerProcessor implements Config private array $mapping = []; private bool $removeUnmapped = false; private bool $recursive = true; + private ?string $case = null; public function configure(array $options): void { @@ -25,6 +26,7 @@ public function configure(array $options): void $this->mapping = $options['mapping']; $this->removeUnmapped = $options['removeUnmapped'] ?? $this->removeUnmapped; $this->recursive = $options['recursive'] ?? $this->recursive; + $this->case = $options['case'] ?? null; // Opcional } public function process(mixed $input): array @@ -35,7 +37,9 @@ public function process(mixed $input): array return []; } - return $this->mapArray($input); + $mappedArray = $this->mapArray($input); + + return $this->case ? $this->transformArrayKeys($mappedArray, $this->case) : $mappedArray; } private function mapArray(array $array): array @@ -43,18 +47,13 @@ private function mapArray(array $array): array $result = []; foreach ($array as $key => $value) { - if (is_array($value) && $this->recursive) { - $result[$key] = $this->mapArray($value); - continue; - } - $mappedKey = $this->mapping[$key] ?? $key; if ($this->removeUnmapped && !isset($this->mapping[$key])) { continue; } - $result[$mappedKey] = $value; + $result[$mappedKey] = is_array($value) && $this->recursive ? $this->mapArray($value) : $value; } return $result; diff --git a/src/Processor/Data/DateTransformer.php b/src/Processor/Data/DateTransformer.php index d5145b2..394fcc9 100644 --- a/src/Processor/Data/DateTransformer.php +++ b/src/Processor/Data/DateTransformer.php @@ -5,52 +5,90 @@ namespace KaririCode\Transformer\Processor\Data; use KaririCode\Contract\Processor\ConfigurableProcessor; +use KaririCode\Transformer\Exception\DateTransformerException; use KaririCode\Transformer\Processor\AbstractTransformerProcessor; -class DateTransformer extends AbstractTransformerProcessor implements ConfigurableProcessor +final class DateTransformer extends AbstractTransformerProcessor implements ConfigurableProcessor { - private string $inputFormat = 'Y-m-d'; - private string $outputFormat = 'Y-m-d'; - private ?string $inputTimezone = null; - private ?string $outputTimezone = null; + private const DEFAULT_FORMAT = 'Y-m-d'; + private const ERROR_INVALID_STRING = 'notString'; + private const ERROR_INVALID_DATE = 'invalidDate'; + + private string $inputFormat = self::DEFAULT_FORMAT; + private string $outputFormat = self::DEFAULT_FORMAT; + private ?\DateTimeZone $inputTimezone = null; + private ?\DateTimeZone $outputTimezone = null; public function configure(array $options): void { - $this->inputFormat = $options['inputFormat'] ?? $this->inputFormat; - $this->outputFormat = $options['outputFormat'] ?? $this->outputFormat; - $this->inputTimezone = $options['inputTimezone'] ?? $this->inputTimezone; - $this->outputTimezone = $options['outputTimezone'] ?? $this->outputTimezone; + $this->configureFormats($options); + $this->configureTimezones($options); } public function process(mixed $input): string { - if (!is_string($input)) { - $this->setInvalid('notString'); - + if (!$this->isValidInput($input)) { return ''; } try { - $date = $this->createDateTime($input); - - return $this->formatDate($date); - } catch (\Exception $e) { - $this->setInvalid('invalidDate'); + return $this->transformDate($input); + } catch (DateTransformerException) { + $this->setInvalid(self::ERROR_INVALID_DATE); return ''; } } - private function createDateTime(string $input): \DateTime + private function configureFormats(array $options): void + { + $this->inputFormat = $options['inputFormat'] ?? self::DEFAULT_FORMAT; + $this->outputFormat = $options['outputFormat'] ?? self::DEFAULT_FORMAT; + } + + private function configureTimezones(array $options): void { - $date = \DateTime::createFromFormat($this->inputFormat, $input); + $this->inputTimezone = $this->createTimezone($options['inputTimezone'] ?? null); + $this->outputTimezone = $this->createTimezone($options['outputTimezone'] ?? null); + } + + private function createTimezone(?string $timezone): ?\DateTimeZone + { + if (!$timezone) { + return null; + } + + try { + return new \DateTimeZone($timezone); + } catch (\Exception) { + throw DateTransformerException::invalidTimezone($timezone); + } + } - if (false === $date) { - throw new \RuntimeException('Invalid date format'); + private function isValidInput(mixed $input): bool + { + if (is_string($input)) { + return true; } - if ($this->inputTimezone) { - $date->setTimezone(new \DateTimeZone($this->inputTimezone)); + $this->setInvalid(self::ERROR_INVALID_STRING); + + return false; + } + + private function transformDate(string $input): string + { + $date = $this->createDateTime($input); + + return $this->formatDate($date); + } + + private function createDateTime(string $input): \DateTime + { + $date = \DateTime::createFromFormat($this->inputFormat, $input, $this->inputTimezone); + + if (!$date) { + throw DateTransformerException::invalidFormat($this->inputFormat, $input); } return $date; @@ -59,7 +97,11 @@ private function createDateTime(string $input): \DateTime private function formatDate(\DateTime $date): string { if ($this->outputTimezone) { - $date->setTimezone(new \DateTimeZone($this->outputTimezone)); + try { + $date->setTimezone($this->outputTimezone); + } catch (\Exception) { + throw DateTransformerException::invalidDate($date->format('Y-m-d H:i:s')); + } } return $date->format($this->outputFormat); diff --git a/src/Processor/String/MaskTransformer.php b/src/Processor/String/MaskTransformer.php index 2a6d174..5f0a521 100644 --- a/src/Processor/String/MaskTransformer.php +++ b/src/Processor/String/MaskTransformer.php @@ -6,67 +6,132 @@ use KaririCode\Contract\Processor\ConfigurableProcessor; use KaririCode\Transformer\Processor\AbstractTransformerProcessor; -use KaririCode\Transformer\Trait\StringTransformerTrait; class MaskTransformer extends AbstractTransformerProcessor implements ConfigurableProcessor { - use StringTransformerTrait; - - private string $mask = ''; - private string $placeholder = '#'; - private array $customMasks = [ + private const DEFAULT_MASKS = [ 'phone' => '(##) #####-####', 'cpf' => '###.###.###-##', 'cnpj' => '##.###.###/####-##', 'cep' => '#####-###', ]; + private const DEFAULT_PLACEHOLDER = '#'; + + private string $mask = ''; + private string $placeholder = self::DEFAULT_PLACEHOLDER; + private array $customMasks = self::DEFAULT_MASKS; public function configure(array $options): void + { + $this->configureMask($options); + $this->configurePlaceholder($options); + } + + public function process(mixed $input): string + { + if (!$this->isValidInput($input)) { + return ''; + } + + if (!$this->hasMask()) { + return $input; + } + + return $this->applyMask($input); + } + + private function configureMask(array $options): void { if (isset($options['mask'])) { $this->mask = $options['mask']; - } elseif (isset($options['type']) && isset($this->customMasks[$options['type']])) { - $this->mask = $this->customMasks[$options['type']]; + + return; } - $this->placeholder = $options['placeholder'] ?? $this->placeholder; + if (!isset($options['type'])) { + return; + } - if (isset($options['customMasks']) && is_array($options['customMasks'])) { - $this->customMasks = array_merge($this->customMasks, $options['customMasks']); + $this->configureCustomMasks($options); + $this->setMaskFromType($options['type']); + } + + private function configureCustomMasks(array $options): void + { + if (!isset($options['customMasks']) || !is_array($options['customMasks'])) { + return; } + + $this->customMasks = array_merge($this->customMasks, $options['customMasks']); } - public function process(mixed $input): string + private function configurePlaceholder(array $options): void + { + if (!isset($options['placeholder'])) { + return; + } + + $this->placeholder = $options['placeholder']; + } + + private function setMaskFromType(string $type): void + { + if (!isset($this->customMasks[$type])) { + return; + } + + $this->mask = $this->customMasks[$type]; + } + + private function isValidInput(mixed $input): bool { if (!is_string($input)) { $this->setInvalid('notString'); - return ''; + return false; } + return true; + } + + private function hasMask(): bool + { if (empty($this->mask)) { $this->setInvalid('noMask'); - return $input; + return false; } - return $this->applyMask($input); + return true; } private function applyMask(string $input): string { - $result = ''; - $inputPos = 0; - - for ($maskPos = 0; $maskPos < strlen($this->mask) && $inputPos < strlen($input); ++$maskPos) { - if ($this->mask[$maskPos] === $this->placeholder) { - $result .= $input[$inputPos]; - ++$inputPos; - } else { - $result .= $this->mask[$maskPos]; + $maskedValue = ''; + $inputIndex = 0; + $inputLength = strlen($input); + + foreach (str_split($this->mask) as $maskChar) { + $maskedValue .= $this->getMaskedCharacter($maskChar, $input, $inputIndex, $inputLength); + + if ($maskChar === $this->placeholder) { + ++$inputIndex; } } - return $result; + return $maskedValue; + } + + private function getMaskedCharacter(string $maskChar, string $input, int $inputIndex, int $inputLength): string + { + if ($maskChar !== $this->placeholder) { + return $maskChar; + } + + if ($inputIndex >= $inputLength) { + return ''; + } + + return $input[$inputIndex]; } } diff --git a/src/Trait/ArrayTransformerTrait.php b/src/Trait/ArrayTransformerTrait.php index 69cb6df..233fdd3 100644 --- a/src/Trait/ArrayTransformerTrait.php +++ b/src/Trait/ArrayTransformerTrait.php @@ -6,31 +6,29 @@ trait ArrayTransformerTrait { - protected function toCamelCase(string $input): string - { - $input = str_replace(['-', '_'], ' ', $input); - $input = ucwords($input); - $input = str_replace(' ', '', $input); - - return lcfirst($input); - } + use StringTransformerTrait; - protected function toPascalCase(string $input): string + protected function transformArrayKeys(array $array, string $case): array { - return ucfirst($this->toCamelCase($input)); - } + $result = []; - protected function toSnakeCase(string $input): string - { - $pattern = '/([a-z0-9])([A-Z])/'; - $input = preg_replace($pattern, '$1_$2', $input); - $input = str_replace(['-', ' '], '_', $input); + foreach ($array as $key => $value) { + $transformedKey = $this->transformKeyByCase((string) $key, $case); + + $result[$transformedKey] = is_array($value) ? $this->transformArrayKeys($value, $case) : $value; + } - return strtolower($input); + return $result; } - protected function toKebabCase(string $input): string + private function transformKeyByCase(string $key, string $case): string { - return str_replace('_', '-', $this->toSnakeCase($input)); + return match ($case) { + 'camel' => $this->toCamelCase($key), + 'snake' => $this->toSnakeCase($key), + 'pascal' => $this->toPascalCase($key), + 'kebab' => $this->toKebabCase($key), + default => $key, + }; } } diff --git a/src/Trait/StringTransformerTrait.php b/src/Trait/StringTransformerTrait.php index b4c9cd2..9847d45 100644 --- a/src/Trait/StringTransformerTrait.php +++ b/src/Trait/StringTransformerTrait.php @@ -30,6 +30,7 @@ protected function toSentenceCase(string $input): string protected function toCamelCase(string $input): string { + $input = $this->removeAccents($input); $input = str_replace(['-', '_'], ' ', $input); $input = ucwords($input); $input = str_replace(' ', '', $input); @@ -39,13 +40,16 @@ protected function toCamelCase(string $input): string protected function toPascalCase(string $input): string { + $input = $this->removeAccents($input); + return ucfirst($this->toCamelCase($input)); } protected function toSnakeCase(string $input): string { - $pattern = '/([a-z0-9])([A-Z])/'; - $input = preg_replace($pattern, '$1_$2', $input); + $input = $this->removeAccents($input); + $input = preg_replace('/([A-Z])([A-Z][a-z])/', '$1_$2', $input); + $input = preg_replace('/([a-z0-9])([A-Z])/', '$1_$2', $input); $input = str_replace(['-', ' '], '_', $input); return strtolower($input); @@ -55,4 +59,11 @@ protected function toKebabCase(string $input): string { return str_replace('_', '-', $this->toSnakeCase($input)); } + + private function removeAccents(string $string): string + { + $string = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $string); + + return preg_replace('/[^A-Za-z0-9_\- ]/', '', $string); + } } diff --git a/src/Transformer.php b/src/Transformer.php index 6ca2929..ebcbc66 100644 --- a/src/Transformer.php +++ b/src/Transformer.php @@ -45,52 +45,3 @@ public function transform(mixed $object): TransformationResult ); } } - -namespace KaririCode\Transformer\Processor\Composite; - -use KaririCode\Contract\Processor\ConfigurableProcessor; -use KaririCode\Transformer\Processor\AbstractTransformerProcessor; - -class ChainTransformer extends AbstractTransformerProcessor implements ConfigurableProcessor -{ - /** @var array */ - private array $transformers = []; - - private bool $stopOnError = true; - - public function configure(array $options): void - { - if (isset($options['transformers']) && is_array($options['transformers'])) { - foreach ($options['transformers'] as $transformer) { - if ($transformer instanceof AbstractTransformerProcessor) { - $this->transformers[] = $transformer; - } - } - } - - $this->stopOnError = $options['stopOnError'] ?? $this->stopOnError; - } - - public function process(mixed $input): mixed - { - $result = $input; - - foreach ($this->transformers as $transformer) { - try { - $result = $transformer->process($result); - - if (!$transformer->isValid() && $this->stopOnError) { - $this->setInvalid($transformer->getErrorKey()); - break; - } - } catch (\Exception $e) { - if ($this->stopOnError) { - $this->setInvalid('transformationError'); - break; - } - } - } - - return $result; - } -} diff --git a/tests/Exception/TransformerExceptionTest.php b/tests/Exception/TransformerExceptionTest.php new file mode 100644 index 0000000..c2042a8 --- /dev/null +++ b/tests/Exception/TransformerExceptionTest.php @@ -0,0 +1,156 @@ +assertInstanceOf(TransformerException::class, $exception); + $this->assertEquals($expectedCode, $exception->getCode()); + $this->assertStringContainsString($expectedType, $exception->getErrorCode()); + $this->assertMatchesRegularExpression($expectedPattern, $exception->getMessage()); + } + + public static function exceptionProvider(): array + { + return [ + 'invalid input' => [ + 'invalidInput', + ['string', 'integer'], + 5001, + 'INVALID_INPUT_TYPE', + '/Expected string, got integer/', + ], + 'invalid format' => [ + 'invalidFormat', + ['Y-m-d', '2024/01/01'], + 5002, + 'INVALID_FORMAT', + '/Expected format Y-m-d, got 2024\/01\/01/', + ], + 'invalid type' => [ + 'invalidType', + ['array'], + 5003, + 'INVALID_TYPE', + '/Expected array/', + ], + ]; + } + + /** + * @dataProvider exceptionMessageProvider + */ + public function testExceptionMessages(string $method, array $params, array $expectations): void + { + $exception = call_user_func_array([TransformerException::class, $method], $params); + $message = $exception->getMessage(); + + foreach ($expectations as $expected) { + $this->assertStringContainsString($expected, $message); + } + } + + public static function exceptionMessageProvider(): array + { + return [ + 'invalid input detailed message' => [ + 'invalidInput', + ['array', 'string'], + ['Expected array', 'got string'], + ], + 'invalid format detailed message' => [ + 'invalidFormat', + ['JSON', 'XML'], + ['Expected format JSON', 'got XML'], + ], + 'invalid type detailed message' => [ + 'invalidType', + ['integer'], + ['Expected integer'], + ], + ]; + } + + /** + * @dataProvider exceptionCodeProvider + */ + public function testExceptionCodes(string $method, array $params, int $expectedCode): void + { + $exception = call_user_func_array([TransformerException::class, $method], $params); + $this->assertEquals($expectedCode, $exception->getCode()); + } + + public static function exceptionCodeProvider(): array + { + return [ + 'invalid input code' => ['invalidInput', ['string', 'integer'], 5001], + 'invalid format code' => ['invalidFormat', ['Y-m-d', '2024/01/01'], 5002], + 'invalid type code' => ['invalidType', ['array'], 5003], + ]; + } + + public function testExceptionHierarchy(): void + { + $exception = TransformerException::invalidInput('string', 'integer'); + $this->assertInstanceOf(\KaririCode\Exception\AbstractException::class, $exception); + } + + public function testCustomExceptionCreation(): void + { + $exception = TransformerException::invalidInput('string', 'integer'); + + $this->assertInstanceOf(TransformerException::class, $exception); + $this->assertEquals(5001, $exception->getCode()); + $this->assertEquals('INVALID_INPUT_TYPE', $exception->getErrorCode()); + $this->assertStringContainsString('Expected string, got integer', $exception->getMessage()); + } + + /** + * @dataProvider exceptionInstancesProvider + */ + public function testDifferentExceptionInstances(string $method, array $params): void + { + $exception = call_user_func_array([TransformerException::class, $method], $params); + + $this->assertInstanceOf(TransformerException::class, $exception); + $this->assertInstanceOf(\Exception::class, $exception); + $this->assertInstanceOf(\Throwable::class, $exception); + } + + public static function exceptionInstancesProvider(): array + { + return [ + 'invalid input instance' => ['invalidInput', ['string', 'integer']], + 'invalid format instance' => ['invalidFormat', ['Y-m-d', '2024/01/01']], + 'invalid type instance' => ['invalidType', ['array']], + ]; + } + + public function testExceptionProperties(): void + { + $exception = TransformerException::invalidInput('string', 'integer'); + + $this->assertIsInt($exception->getCode()); + $this->assertIsString($exception->getMessage()); + $this->assertIsString($exception->getErrorCode()); + $this->assertNotEmpty($exception->getMessage()); + $this->assertNotEmpty($exception->getErrorCode()); + } +} diff --git a/tests/Processor/AbstractTransformerProcessorTest.php b/tests/Processor/AbstractTransformerProcessorTest.php new file mode 100644 index 0000000..2bcd94a --- /dev/null +++ b/tests/Processor/AbstractTransformerProcessorTest.php @@ -0,0 +1,192 @@ +processor = new class extends AbstractTransformerProcessor { + public mixed $returnValue; + public bool $shouldThrow = false; + + public function process(mixed $input): mixed + { + if ($this->shouldThrow) { + throw new \Exception('Test exception'); + } + + return $this->returnValue ?? $input; + } + + public function setInvalidPublic(string $errorKey): void + { + $this->setInvalid($errorKey); + } + + public function guardAgainstInvalidTypePublic(mixed $input, string $expectedType): void + { + if (get_debug_type($input) !== $expectedType) { + throw TransformerException::invalidType($expectedType); + } + } + }; + } + + public function testClassImplementsCorrectInterfaces(): void + { + $this->assertInstanceOf(Processor::class, $this->processor); + $this->assertInstanceOf(ValidatableProcessor::class, $this->processor); + } + + public function testInitialState(): void + { + $this->assertTrue($this->processor->isValid()); + $this->assertEmpty($this->processor->getErrorKey()); + } + + public function testValidStateAfterSuccessfulProcess(): void + { + $this->processor->process('test'); + $this->assertTrue($this->processor->isValid()); + $this->assertEmpty($this->processor->getErrorKey()); + } + + public function testInvalidStateAfterError(): void + { + $errorKey = 'test_error'; + $this->processor->setInvalidPublic($errorKey); + + $this->assertFalse($this->processor->isValid()); + $this->assertEquals($errorKey, $this->processor->getErrorKey()); + } + + public function testResetResetsState(): void + { + $this->processor->setInvalidPublic('error'); + $this->processor->reset(); + + $this->assertTrue($this->processor->isValid()); + $this->assertEmpty($this->processor->getErrorKey()); + } + + /** + * @dataProvider invalidTypeProvider + */ + public function testGuardAgainstInvalidTypeThrowsException(mixed $input, string $expectedType): void + { + $this->expectException(TransformerException::class); + $this->processor->guardAgainstInvalidTypePublic($input, $expectedType); + } + + public static function invalidTypeProvider(): array + { + return [ + 'string as integer' => ['42', 'integer'], + 'integer as string' => [42, 'string'], + 'array as object' => [[], 'object'], + 'object as array' => [new \stdClass(), 'array'], + 'null as string' => [null, 'string'], + 'boolean as integer' => [true, 'integer'], + ]; + } + + /** + * @dataProvider validTypeProvider + */ + public function testGuardAgainstValidType(mixed $input, string $expectedType): void + { + $actualType = get_debug_type($input); + $this->assertEquals($expectedType, $actualType); + + try { + $this->processor->guardAgainstInvalidTypePublic($input, $expectedType); + $this->assertTrue(true); // Se chegou aqui, não lançou exceção + } catch (TransformerException $e) { + $this->fail('Should not throw exception for valid type'); + } + } + + public static function validTypeProvider(): array + { + return [ + 'string type' => ['test', 'string'], + 'integer type' => [42, 'int'], + 'float type' => [3.14, 'float'], + 'boolean type' => [true, 'bool'], + 'array type' => [[], 'array'], + 'object type' => [new \stdClass(), 'stdClass'], + 'null type' => [null, 'null'], + ]; + } + + /** + * @dataProvider processorStateProvider + */ + public function testProcessorStateTransitions(string $errorKey, bool $expectedValidity): void + { + $this->processor->setInvalidPublic($errorKey); + + $this->assertEquals($errorKey, $this->processor->getErrorKey()); + $this->assertEquals($expectedValidity, $this->processor->isValid()); + + $this->processor->reset(); + $this->assertTrue($this->processor->isValid()); + $this->assertEmpty($this->processor->getErrorKey()); + } + + public static function processorStateProvider(): array + { + return [ + 'simple error' => ['validation_error', false], + 'complex error key' => ['nested.validation.error', false], + 'numeric error' => ['error_404', false], + ]; + } + + /** + * @dataProvider processInputProvider + */ + public function testProcessWithDifferentInputs(mixed $input, mixed $expectedOutput): void + { + $this->processor->returnValue = $expectedOutput; + $result = $this->processor->process($input); + + $this->assertEquals($expectedOutput, $result); + $this->assertTrue($this->processor->isValid()); + } + + public static function processInputProvider(): array + { + return [ + 'string input/output' => ['input', 'processed'], + 'array transformation' => [['input'], ['processed']], + 'null handling' => [null, null], + 'numeric transformation' => [42, 84], + 'boolean transformation' => [true, false], + ]; + } + + public function testProcessingExceptionHandling(): void + { + $this->processor->shouldThrow = true; + + try { + $this->processor->process('test'); + } catch (\Exception $e) { + $this->assertEquals('Test exception', $e->getMessage()); + } + + $this->assertTrue($this->processor->isValid(), 'Processor should remain valid after caught exception'); + } +} diff --git a/tests/Processor/Array/ArrayFlattenTransformerTest.php b/tests/Processor/Array/ArrayFlattenTransformerTest.php new file mode 100644 index 0000000..b2b3ee0 --- /dev/null +++ b/tests/Processor/Array/ArrayFlattenTransformerTest.php @@ -0,0 +1,73 @@ +transformer = new ArrayFlattenTransformer(); + } + + /** + * @dataProvider arrayFlattenProvider + */ + public function testArrayFlatten(array $input, array $config, array $expected, bool $shouldBeValid): void + { + $this->transformer->configure($config); + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($shouldBeValid, $this->transformer->isValid()); + } + + public static function arrayFlattenProvider(): array + { + return [ + 'simple nested array' => [ + ['a' => ['b' => 1]], + [], + ['a.b' => 1], + true, + ], + 'multiple levels' => [ + ['a' => ['b' => ['c' => 1]]], + [], + ['a.b.c' => 1], + true, + ], + 'custom separator' => [ + ['a' => ['b' => 1]], + ['separator' => '_'], + ['a_b' => 1], + true, + ], + 'limited depth' => [ + ['a' => ['b' => ['c' => 1]]], + ['depth' => 1], + ['a.b' => ['c' => 1]], + true, + ], + 'multiple keys' => [ + ['a' => ['b' => 1, 'c' => 2]], + [], + ['a.b' => 1, 'a.c' => 2], + true, + ], + ]; + } + + public function testInvalidInput(): void + { + $result = $this->transformer->process('not an array'); + $this->assertEmpty($result); + $this->assertFalse($this->transformer->isValid()); + } +} diff --git a/tests/Processor/Array/ArrayGroupTransformerTest.php b/tests/Processor/Array/ArrayGroupTransformerTest.php new file mode 100644 index 0000000..273e911 --- /dev/null +++ b/tests/Processor/Array/ArrayGroupTransformerTest.php @@ -0,0 +1,109 @@ +transformer = new ArrayGroupTransformer(); + } + + /** + * @dataProvider groupArrayProvider + */ + public function testGroupArray(array $input, array $config, array $expected, bool $shouldBeValid): void + { + $this->transformer->configure($config); + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($shouldBeValid, $this->transformer->isValid()); + } + + public static function groupArrayProvider(): array + { + return [ + 'simple grouping' => [ + [ + ['type' => 'a', 'value' => 1], + ['type' => 'a', 'value' => 2], + ['type' => 'b', 'value' => 3], + ], + ['groupBy' => 'type'], + [ + 'a' => [ + ['type' => 'a', 'value' => 1], + ['type' => 'a', 'value' => 2], + ], + 'b' => [ + ['type' => 'b', 'value' => 3], + ], + ], + true, + ], + 'preserve keys' => [ + [ + 0 => ['type' => 'a', 'value' => 1], + 1 => ['type' => 'a', 'value' => 2], + ], + ['groupBy' => 'type', 'preserveKeys' => true], + [ + 'a' => [ + 0 => ['type' => 'a', 'value' => 1], + 1 => ['type' => 'a', 'value' => 2], + ], + ], + true, + ], + 'missing group key' => [ + [ + ['type' => 'a', 'value' => 1], + ['value' => 2], + ], + ['groupBy' => 'type'], + [ + 'a' => [ + ['type' => 'a', 'value' => 1], + ], + ], + true, + ], + 'non-array items' => [ + [ + ['type' => 'a'], + 'invalid', + ], + ['groupBy' => 'type'], + [ + 'a' => [ + ['type' => 'a'], + ], + ], + true, + ], + ]; + } + + public function testInvalidInput(): void + { + $this->transformer->configure(['groupBy' => 'type']); + $result = $this->transformer->process('not an array'); + + $this->assertEmpty($result); + $this->assertFalse($this->transformer->isValid()); + } + + public function testMissingGroupByConfig(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->transformer->configure([]); + } +} diff --git a/tests/Processor/Array/ArrayKeyTransformerTest.php b/tests/Processor/Array/ArrayKeyTransformerTest.php new file mode 100644 index 0000000..46f1379 --- /dev/null +++ b/tests/Processor/Array/ArrayKeyTransformerTest.php @@ -0,0 +1,67 @@ +transformer = new ArrayKeyTransformer(); + } + + /** + * @dataProvider arrayKeyTransformationProvider + */ + public function testArrayKeyTransformation(array $input, array $config, array $expected, bool $shouldBeValid): void + { + $this->transformer->configure($config); + $result = $this->transformer->process($input); + + $this->assertSame($expected, $result); + $this->assertSame($shouldBeValid, $this->transformer->isValid()); + } + + public static function arrayKeyTransformationProvider(): array + { + return [ + 'to snake case' => [ + ['helloWorld' => 1, 'goodBye' => 2], + ['case' => 'snake'], + ['hello_world' => 1, 'good_bye' => 2], + true, + ], + 'to camel case' => [ + ['hello_world' => 1, 'good_bye' => 2], + ['case' => 'camel'], + ['helloWorld' => 1, 'goodBye' => 2], + true, + ], + 'nested arrays' => [ + ['helloWorld' => ['nestedKey' => 1]], + ['case' => 'snake', 'recursive' => true], + ['hello_world' => ['nested_key' => 1]], + true, + ], + 'non-recursive' => [ + ['helloWorld' => ['nestedKey' => 1]], + ['case' => 'snake', 'recursive' => false], + ['hello_world' => ['nestedKey' => 1]], + true, + ], + ]; + } + + public function testInvalidInput(): void + { + $result = $this->transformer->process('not an array'); + $this->assertEmpty($result); + $this->assertFalse($this->transformer->isValid()); + } +} diff --git a/tests/Processor/Array/ArrayMapTransformerTest.php b/tests/Processor/Array/ArrayMapTransformerTest.php new file mode 100644 index 0000000..15fb4ed --- /dev/null +++ b/tests/Processor/Array/ArrayMapTransformerTest.php @@ -0,0 +1,96 @@ +transformer = new ArrayMapTransformer(); + } + + /** + * @dataProvider arrayMapProvider + */ + public function testArrayMap(array $input, array $config, array $expected, bool $shouldBeValid): void + { + $this->transformer->configure($config); + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($shouldBeValid, $this->transformer->isValid()); + } + + public static function arrayMapProvider(): array + { + return [ + 'simple mapping' => [ + ['old_key' => 'value'], + ['mapping' => ['old_key' => 'new_key']], + ['new_key' => 'value'], + true, + ], + 'multiple keys' => [ + ['key1' => 'value1', 'key2' => 'value2'], + ['mapping' => ['key1' => 'new1', 'key2' => 'new2']], + ['new1' => 'value1', 'new2' => 'value2'], + true, + ], + 'nested arrays' => [ + ['key1' => ['nested' => 'value']], + [ + 'mapping' => ['key1' => 'new1'], + 'recursive' => true, + ], + ['new1' => ['nested' => 'value']], + true, + ], + 'remove unmapped' => [ + ['key1' => 'value1', 'key2' => 'value2'], + [ + 'mapping' => ['key1' => 'new1'], + 'removeUnmapped' => true, + ], + ['new1' => 'value1'], + true, + ], + 'nested with recursion disabled' => [ + ['key1' => ['nested' => 'value']], + [ + 'mapping' => ['key1' => 'new1'], + 'recursive' => false, + ], + ['new1' => ['nested' => 'value']], + true, + ], + ]; + } + + public function testInvalidInput(): void + { + $this->transformer->configure(['mapping' => ['old' => 'new']]); + $result = $this->transformer->process('not an array'); + + $this->assertEmpty($result); + $this->assertFalse($this->transformer->isValid()); + } + + public function testMissingMappingConfig(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->transformer->configure([]); + } + + public function testInvalidMappingConfig(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->transformer->configure(['mapping' => 'invalid']); + } +} diff --git a/tests/Processor/Composite/ChainTransformerTest.php b/tests/Processor/Composite/ChainTransformerTest.php new file mode 100644 index 0000000..09ee3e1 --- /dev/null +++ b/tests/Processor/Composite/ChainTransformerTest.php @@ -0,0 +1,269 @@ +transformer = new ChainTransformer(); + } + + /** + * @dataProvider processInputProvider + */ + public function testProcessWithDifferentInputTypes(mixed $input, mixed $expected): void + { + $mockTransformer = $this->createTypedMockTransformer($input, $expected); + $this->transformer->configure(['transformers' => [$mockTransformer]]); + + $this->assertEquals($expected, $this->transformer->process($input)); + $this->assertTrue($this->transformer->isValid()); + } + + public static function processInputProvider(): array + { + return [ + 'string input' => ['test', 'processed'], + 'integer input' => [42, 84], + 'float input' => [3.14, 6.28], + 'array input' => [['a' => 1], ['a' => 2]], + 'null input' => [null, null], + 'boolean input' => [true, false], + 'object input' => [new \stdClass(), new \stdClass()], + ]; + } + + /** + * @dataProvider chainConfigurationProvider + */ + public function testProcessWithDifferentChainConfigurations( + array $transformerConfigs, + mixed $input, + mixed $expected, + bool $expectedValidity, + string $expectedError + ): void { + $transformers = array_map( + fn (array $config) => $this->createConfiguredMockTransformer(...$config), + $transformerConfigs + ); + + $this->transformer->configure(['transformers' => $transformers]); + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($expectedValidity, $this->transformer->isValid()); + $this->assertEquals($expectedError, $this->transformer->getErrorKey()); + } + + public static function chainConfigurationProvider(): array + { + return [ + 'successful chain' => [ + [ + ['input', 'first', true, ''], + ['first', 'second', true, ''], + ['second', 'final', true, ''], + ], + 'input', + 'final', + true, + '', + ], + 'chain with middle error' => [ + [ + ['input', 'first', true, ''], + ['first', 'error', false, 'middle_error'], + ['error', 'final', true, ''], + ], + 'input', + 'error', + false, + 'middle_error', + ], + 'empty transformers' => [ + [], + 'input', + 'input', + true, + '', + ], + ]; + } + + /** + * @dataProvider errorHandlingConfigurationProvider + */ + public function testProcessWithDifferentErrorHandlingConfigurations( + bool $stopOnError, + array $transformerConfigs, + mixed $input, + mixed $expected, + bool $expectedValidity + ): void { + $transformers = array_map( + fn (array $config) => $this->createConfiguredMockTransformer(...$config), + $transformerConfigs + ); + + $this->transformer->configure([ + 'transformers' => $transformers, + 'stopOnError' => $stopOnError, + ]); + + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($expectedValidity, $this->transformer->isValid()); + } + + public static function errorHandlingConfigurationProvider(): array + { + return [ + 'continue on error' => [ + false, + [ + ['input', 'first', false, 'error1'], + ['first', 'second', true, ''], + ['second', 'final', true, ''], + ], + 'input', + 'final', + true, + ], + 'stop on error' => [ + true, + [ + ['input', 'first', false, 'error1'], + ['first', 'second', true, ''], + ], + 'input', + 'first', + false, + ], + ]; + } + + /** + * @dataProvider exceptionHandlingProvider + */ + public function testProcessWithExceptionHandling( + bool $stopOnError, + array $transformerConfigs, + string $input, + string $expected + ): void { + $transformers = []; + foreach ($transformerConfigs as $config) { + $transformers[] = $config['throws'] + ? $this->createExceptionTransformer() + : $this->createConfiguredMockTransformer($config['input'], $config['output'], true, ''); + } + + $this->transformer->configure([ + 'transformers' => $transformers, + 'stopOnError' => $stopOnError, + ]); + + $result = $this->transformer->process($input); + $this->assertEquals($expected, $result); + } + + public static function exceptionHandlingProvider(): array + { + return [ + 'exception with stop' => [ + true, + [ + ['throws' => true], + ['input' => 'input', 'output' => 'final', 'throws' => false], + ], + 'input', + 'input', + ], + 'exception without stop' => [ + false, + [ + ['throws' => true], + ['input' => 'input', 'output' => 'final', 'throws' => false], + ], + 'input', + 'final', + ], + 'multiple exceptions without stop' => [ + false, + [ + ['throws' => true], + ['throws' => true], + ['input' => 'input', 'output' => 'final', 'throws' => false], + ], + 'input', + 'final', + ], + ]; + } + + public function testInvalidConfigurationTypes(): void + { + $invalidTransformers = [ + new \stdClass(), + 'not a transformer', + 42, + null, + ]; + + $this->transformer->configure(['transformers' => $invalidTransformers]); + $result = $this->transformer->process('input'); + + $this->assertSame('input', $result); + $this->assertTrue($this->transformer->isValid()); + } + + private function createTypedMockTransformer(mixed $input, mixed $output): AbstractTransformerProcessor + { + $mock = $this->createMock(AbstractTransformerProcessor::class); + $mock->method('process') + ->with($this->equalTo($input)) + ->willReturn($output); + $mock->method('isValid') + ->willReturn(true); + + return $mock; + } + + private function createConfiguredMockTransformer( + mixed $expectedInput, + mixed $output, + bool $isValid = true, + string $errorKey = '' + ): AbstractTransformerProcessor { + $mock = $this->createMock(AbstractTransformerProcessor::class); + $mock->method('process') + ->with($this->equalTo($expectedInput)) + ->willReturn($output); + $mock->method('isValid') + ->willReturn($isValid); + $mock->method('getErrorKey') + ->willReturn($errorKey); + + return $mock; + } + + private function createExceptionTransformer(): AbstractTransformerProcessor + { + $mock = $this->createMock(AbstractTransformerProcessor::class); + $mock->method('process') + ->willThrowException(new \Exception('Test exception')); + + return $mock; + } +} diff --git a/tests/Processor/Composite/ConditionalTransformerTest.php b/tests/Processor/Composite/ConditionalTransformerTest.php new file mode 100644 index 0000000..f6d1162 --- /dev/null +++ b/tests/Processor/Composite/ConditionalTransformerTest.php @@ -0,0 +1,121 @@ +transformer = new ConditionalTransformer(); + } + + /** + * @dataProvider conditionalTransformProvider + */ + public function testConditionalTransform( + mixed $input, + bool $conditionResult, + mixed $transformedValue, + array $config, + mixed $expected, + bool $shouldBeValid + ): void { + $mockTransformer = $this->createConfiguredMockTransformer($transformedValue, $shouldBeValid); + + $config['transformer'] = $mockTransformer; + $config['condition'] = fn ($value) => $conditionResult; + + $this->transformer->configure($config); + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($shouldBeValid, $this->transformer->isValid()); + } + + public static function conditionalTransformProvider(): array + { + return [ + 'condition true' => [ + 'input', + true, + 'transformed', + [], + 'transformed', + true, + ], + 'condition false' => [ + 'input', + false, + 'transformed', + [], + 'input', + true, + ], + 'condition true with default' => [ + 'input', + true, + 'transformed', + ['defaultValue' => 'default'], + 'transformed', + true, + ], + 'condition false with default' => [ + 'input', + false, + 'transformed', + ['defaultValue' => 'default'], + 'default', + true, + ], + 'transform error with default' => [ + 'input', + true, + 'transformed', + [ + 'defaultValue' => 'default', + 'useDefaultOnError' => true, + ], + 'default', + false, + ], + ]; + } + + public function testMissingTransformerConfig(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->transformer->configure(['condition' => fn () => true]); + } + + public function testMissingConditionConfig(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->transformer->configure(['transformer' => $this->createMock(AbstractTransformerProcessor::class)]); + } + + public function testInvalidConditionCallback(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->transformer->configure([ + 'transformer' => $this->createMock(AbstractTransformerProcessor::class), + 'condition' => 'not a callback', + ]); + } + + private function createConfiguredMockTransformer(mixed $output, bool $isValid = true): AbstractTransformerProcessor + { + $mock = $this->createMock(AbstractTransformerProcessor::class); + $mock->method('process')->willReturn($output); + $mock->method('isValid')->willReturn($isValid); + + return $mock; + } +} diff --git a/tests/Processor/Data/DateTransformerTest.php b/tests/Processor/Data/DateTransformerTest.php new file mode 100644 index 0000000..70e84d4 --- /dev/null +++ b/tests/Processor/Data/DateTransformerTest.php @@ -0,0 +1,235 @@ +transformer = new DateTransformer(); + } + + /** + * @dataProvider dateFormatProvider + */ + public function testDateFormat(string $input, array $config, string $expected, bool $shouldBeValid): void + { + $this->transformer->configure($config); + $result = $this->transformer->process($input); + $this->assertEquals($expected, $result); + $this->assertEquals($shouldBeValid, $this->transformer->isValid()); + } + + public static function dateFormatProvider(): array + { + return [ + 'simple format' => [ + '2024-01-01', + ['inputFormat' => 'Y-m-d', 'outputFormat' => 'd/m/Y'], + '01/01/2024', + true, + ], + 'with time' => [ + '2024-01-01 15:30:00', + ['inputFormat' => 'Y-m-d H:i:s', 'outputFormat' => 'd/m/Y H:i'], + '01/01/2024 15:30', + true, + ], + 'timezone conversion' => [ + '2024-07-01 12:00:00', // Usando uma data em julho (sem horário de verão) + [ + 'inputFormat' => 'Y-m-d H:i:s', + 'outputFormat' => 'Y-m-d H:i:s', + 'inputTimezone' => 'UTC', + 'outputTimezone' => 'America/New_York', + ], + '2024-07-01 08:00:00', + true, + ], + 'invalid date' => [ + 'invalid', + ['inputFormat' => 'Y-m-d'], + '', + false, + ], + ]; + } + + /** + * @dataProvider timezoneConversionProvider + */ + public function testTimezoneConversion(string $input, string $inputTz, string $outputTz, string $expected): void + { + $this->transformer->configure([ + 'inputFormat' => 'Y-m-d H:i:s', + 'outputFormat' => 'Y-m-d H:i:s', + 'inputTimezone' => $inputTz, + 'outputTimezone' => $outputTz, + ]); + + $result = $this->transformer->process($input); + $this->assertEquals($expected, $result); + } + + public static function timezoneConversionProvider(): array + { + return [ + 'UTC to EST (winter)' => [ + '2024-01-01 12:00:00', + 'UTC', + 'America/New_York', + '2024-01-01 07:00:00', + ], + 'UTC to EST (summer)' => [ + '2024-07-01 12:00:00', + 'UTC', + 'America/New_York', + '2024-07-01 08:00:00', + ], + 'EST to UTC (winter)' => [ + '2024-01-01 12:00:00', + 'America/New_York', + 'UTC', + '2024-01-01 17:00:00', + ], + 'EST to UTC (summer)' => [ + '2024-07-01 12:00:00', + 'America/New_York', + 'UTC', + '2024-07-01 16:00:00', + ], + ]; + } + + public function testInvalidInput(): void + { + $result = $this->transformer->process(123); + $this->assertEmpty($result); + $this->assertFalse($this->transformer->isValid()); + } + + /** + * @dataProvider invalidTimezoneProvider + */ + public function testInvalidTimezone(string $timezone): void + { + $this->expectException(DateTransformerException::class); + $this->expectExceptionCode(5101); + $this->expectExceptionMessage("Invalid timezone: {$timezone}"); + + $this->transformer->configure([ + 'inputFormat' => 'Y-m-d', + 'inputTimezone' => $timezone, + ]); + } + + public static function invalidTimezoneProvider(): array + { + return [ + 'invalid timezone name' => ['Invalid/Timezone'], + 'numeric timezone' => ['123'], + 'special chars timezone' => ['UTC@#$'], + 'non-existent timezone' => ['America/InvalidCity'], + ]; + } + + public function testEmptyTimezoneIsValid(): void + { + $this->transformer->configure([ + 'inputFormat' => 'Y-m-d', + 'inputTimezone' => '', + ]); + + $result = $this->transformer->process('2024-01-01'); + + $this->assertEquals('2024-01-01', $result); + $this->assertTrue($this->transformer->isValid()); + } + + public function testNullTimezoneIsValid(): void + { + $this->transformer->configure([ + 'inputFormat' => 'Y-m-d', + 'inputTimezone' => null, + ]); + + $result = $this->transformer->process('2024-01-01'); + + $this->assertEquals('2024-01-01', $result); + $this->assertTrue($this->transformer->isValid()); + } + + /** + * @dataProvider invalidFormatProvider + */ + public function testInvalidFormat(string $input, string $format): void + { + $this->transformer->configure(['inputFormat' => $format]); + $result = $this->transformer->process($input); + + $this->assertEmpty($result); + $this->assertFalse($this->transformer->isValid()); + } + + public static function invalidFormatProvider(): array + { + return [ + 'wrong format completely' => ['2024-01-01', 'd-m-Y'], + 'missing components' => ['2024-01', 'Y-m-d'], + 'invalid format chars' => ['2024-01-01', 'X-Y-Z'], + 'empty format' => ['2024-01-01', ''], + ]; + } + + /** + * @dataProvider invalidInputTypeProvider + */ + public function testInvalidInputType(mixed $input): void + { + $result = $this->transformer->process($input); + $this->assertEmpty($result); + $this->assertFalse($this->transformer->isValid()); + } + + public static function invalidInputTypeProvider(): array + { + return [ + 'integer input' => [123], + 'float input' => [123.45], + 'boolean input' => [true], + 'array input' => [['2024-01-01']], + 'null input' => [null], + 'object input' => [new \stdClass()], + ]; + } + + public function testConfigureWithoutTimezone(): void + { + $input = '2024-01-01'; + $this->transformer->configure(['inputFormat' => 'Y-m-d']); + + $result = $this->transformer->process($input); + + $this->assertEquals('2024-01-01', $result); + $this->assertTrue($this->transformer->isValid()); + } + + public function testConfigureWithEmptyOptions(): void + { + $input = '2024-01-01'; + $this->transformer->configure([]); + + $result = $this->transformer->process($input); + + $this->assertEquals('2024-01-01', $result); + $this->assertTrue($this->transformer->isValid()); + } +} diff --git a/tests/Processor/Data/JsonTransformerTest.php b/tests/Processor/Data/JsonTransformerTest.php new file mode 100644 index 0000000..3d61514 --- /dev/null +++ b/tests/Processor/Data/JsonTransformerTest.php @@ -0,0 +1,96 @@ +transformer = new JsonTransformer(); + } + + /** + * @dataProvider jsonDecodeProvider + */ + public function testJsonDecode(string $input, array $config, mixed $expected, bool $shouldBeValid): void + { + $this->transformer->configure($config); + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($shouldBeValid, $this->transformer->isValid()); + } + + public static function jsonDecodeProvider(): array + { + return [ + 'simple array' => [ + '{"key":"value"}', + ['assoc' => true], + ['key' => 'value'], + true, + ], + 'nested array' => [ + '{"key":{"nested":"value"}}', + ['assoc' => true], + ['key' => ['nested' => 'value']], + true, + ], + 'as object' => [ + '{"key":"value"}', + ['assoc' => false], + (object) ['key' => 'value'], + true, + ], + 'invalid json' => [ + '{invalid}', + ['assoc' => true], + [], + false, + ], + ]; + } + + /** + * @dataProvider jsonEncodeProvider + */ + public function testJsonEncode(array $input, array $config, string $expected, bool $shouldBeValid): void + { + $this->transformer->configure(array_merge(['returnString' => true], $config)); + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($shouldBeValid, $this->transformer->isValid()); + } + + public static function jsonEncodeProvider(): array + { + return [ + 'simple array' => [ + ['key' => 'value'], + [], + '{"key":"value"}', + true, + ], + 'nested array' => [ + ['key' => ['nested' => 'value']], + [], + '{"key":{"nested":"value"}}', + true, + ], + 'with options' => [ + ['key' => 'value'], + ['encodeOptions' => JSON_PRETTY_PRINT], + "{\n \"key\": \"value\"\n}", + true, + ], + ]; + } +} diff --git a/tests/Processor/Data/NumberTransformerTest.php b/tests/Processor/Data/NumberTransformerTest.php new file mode 100644 index 0000000..20736f6 --- /dev/null +++ b/tests/Processor/Data/NumberTransformerTest.php @@ -0,0 +1,72 @@ +transformer = new NumberTransformer(); + } + + /** + * @dataProvider numberFormatProvider + */ + public function testNumberFormat(mixed $input, array $config, mixed $expected, bool $shouldBeValid): void + { + $this->transformer->configure($config); + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($shouldBeValid, $this->transformer->isValid()); + } + + public static function numberFormatProvider(): array + { + return [ + 'simple decimal' => [ + 123.456, + ['decimals' => 2], + 123.46, + true, + ], + 'with thousand separator' => [ + 1234.56, + ['formatAsString' => true, 'thousandsSeparator' => ','], + '1,234.56', + true, + ], + 'custom decimal point' => [ + 1234.56, + ['formatAsString' => true, 'decimalPoint' => ','], + '1234,56', + true, + ], + 'with multiplier' => [ + 100, + ['multiplier' => 1.5], + 150.0, + true, + ], + 'round up' => [ + 123.456, + ['decimals' => 2, 'roundUp' => true], + 123.46, + true, + ], + 'invalid input' => [ + 'invalid', + [], + 0.0, + false, + ], + ]; + } +} diff --git a/tests/Processor/String/CaseTransformerTest.php b/tests/Processor/String/CaseTransformerTest.php new file mode 100644 index 0000000..5a202f8 --- /dev/null +++ b/tests/Processor/String/CaseTransformerTest.php @@ -0,0 +1,91 @@ +transformer = new CaseTransformer(); + } + + /** + * @dataProvider caseTransformationProvider + */ + public function testCaseTransformation(string $input, array $config, string $expected, bool $shouldBeValid): void + { + $this->transformer->configure($config); + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($shouldBeValid, $this->transformer->isValid()); + } + + public static function caseTransformationProvider(): array + { + return [ + 'to lower' => [ + 'Hello World', + ['case' => 'lower'], + 'hello world', + true, + ], + 'to upper' => [ + 'Hello World', + ['case' => 'upper'], + 'HELLO WORLD', + true, + ], + 'to title' => [ + 'hello world', + ['case' => 'title'], + 'Hello World', + true, + ], + 'to camel' => [ + 'hello_world', + ['case' => 'camel'], + 'helloWorld', + true, + ], + 'to pascal' => [ + 'hello_world', + ['case' => 'pascal'], + 'HelloWorld', + true, + ], + 'to snake' => [ + 'helloWorld', + ['case' => 'snake'], + 'hello_world', + true, + ], + 'to kebab' => [ + 'helloWorld', + ['case' => 'kebab'], + 'hello-world', + true, + ], + 'preserve numbers' => [ + 'hello123World', + ['case' => 'snake', 'preserveNumbers' => true], + 'hello123_world', + true, + ], + ]; + } + + public function testInvalidInput(): void + { + $result = $this->transformer->process(123); + $this->assertEmpty($result); + $this->assertFalse($this->transformer->isValid()); + } +} diff --git a/tests/Processor/String/MaskTransformerTest.php b/tests/Processor/String/MaskTransformerTest.php new file mode 100644 index 0000000..9160c5c --- /dev/null +++ b/tests/Processor/String/MaskTransformerTest.php @@ -0,0 +1,90 @@ +transformer = new MaskTransformer(); + } + + /** + * @dataProvider maskProvider + */ + public function testMask(string $input, array $config, string $expected, bool $shouldBeValid): void + { + $this->transformer->configure($config); + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($shouldBeValid, $this->transformer->isValid()); + } + + public static function maskProvider(): array + { + return [ + 'custom mask' => [ + '1234567890', + ['mask' => '(##) ####-####'], + '(12) 3456-7890', + true, + ], + 'phone type' => [ + '12345678901', + ['type' => 'phone'], + '(12) 34567-8901', + true, + ], + 'cpf type' => [ + '12345678901', + ['type' => 'cpf'], + '123.456.789-01', + true, + ], + 'custom placeholder' => [ + 'ABC12345', + [ + 'mask' => '@@@-@@@@@', + 'placeholder' => '@', + ], + 'ABC-12345', + true, + ], + 'custom mask types' => [ + '123456', + [ + 'type' => 'custom', + 'customMasks' => ['custom' => '##-##-##'], + ], + '12-34-56', + true, + ], + ]; + } + + public function testInvalidInput(): void + { + $this->transformer->configure(['mask' => '##-##']); + $result = $this->transformer->process(123); + + $this->assertEmpty($result); + $this->assertFalse($this->transformer->isValid()); + } + + public function testNoMaskConfigured(): void + { + $input = 'test'; + $result = $this->transformer->process($input); + + $this->assertSame($input, $result); + $this->assertFalse($this->transformer->isValid()); + } +} diff --git a/tests/Processor/String/SlugTransformerTest.php b/tests/Processor/String/SlugTransformerTest.php new file mode 100644 index 0000000..e8d6c6a --- /dev/null +++ b/tests/Processor/String/SlugTransformerTest.php @@ -0,0 +1,72 @@ +transformer = new SlugTransformer(); + } + + /** + * @dataProvider slugProvider + */ + public function testSlugGeneration(string $input, array $config, string $expected, bool $shouldBeValid): void + { + $this->transformer->configure($config); + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($shouldBeValid, $this->transformer->isValid()); + } + + public static function slugProvider(): array + { + return [ + 'simple text' => [ + 'Hello World', + [], + 'hello-world', + true, + ], + 'with accents' => [ + 'Café à la crème', + [], + 'cafe-a-la-creme', + true, + ], + 'custom separator' => [ + 'Hello World', + ['separator' => '_'], + 'hello_world', + true, + ], + 'custom replacements' => [ + 'Hello & World @ Home', + ['replacements' => ['&' => 'and', '@' => 'at']], + 'hello-and-world-at-home', + true, + ], + 'preserve case' => [ + 'Hello World', + ['lowercase' => false], + 'Hello-World', + true, + ], + 'empty input' => [ + '', + [], + '', + false, + ], + ]; + } +} diff --git a/tests/Processor/String/TemplateTransformerTest.php b/tests/Processor/String/TemplateTransformerTest.php new file mode 100644 index 0000000..1563b53 --- /dev/null +++ b/tests/Processor/String/TemplateTransformerTest.php @@ -0,0 +1,103 @@ +transformer = new TemplateTransformer(); + } + + /** + * @dataProvider templateProvider + */ + public function testTemplate(array $input, array $config, mixed $expected, bool $shouldBeValid): void + { + $this->transformer->configure($config); + $result = $this->transformer->process($input); + + $this->assertEquals($expected, $result); + $this->assertEquals($shouldBeValid, $this->transformer->isValid()); + } + + public static function templateProvider(): array + { + return [ + 'simple template' => [ + ['name' => 'John'], + ['template' => 'Hello {{name}}!'], + ['name' => 'John', '_rendered' => 'Hello John!'], + true, + ], + 'multiple replacements' => [ + ['name' => 'John', 'age' => '30'], + ['template' => '{{name}} is {{age}} years old'], + ['name' => 'John', 'age' => '30', '_rendered' => 'John is 30 years old'], + true, + ], + 'custom tags' => [ + ['name' => 'John'], + [ + 'template' => 'Hello [name]!', + 'openTag' => '[', + 'closeTag' => ']', + ], + ['name' => 'John', '_rendered' => 'Hello John!'], + true, + ], + 'missing value handler' => [ + ['name' => 'John'], + [ + 'template' => '{{name}} {{missing}}', + 'missingValueHandler' => fn ($key) => "[$key]", + ], + ['name' => 'John', '_rendered' => 'John [missing]'], + true, + ], + 'remove unmatched tags' => [ + ['name' => 'John'], + [ + 'template' => '{{name}} {{missing}}', + 'removeUnmatchedTags' => true, + ], + ['name' => 'John', '_rendered' => 'John '], + true, + ], + 'without data preservation' => [ + ['name' => 'John'], + [ + 'template' => 'Hello {{name}}!', + 'preserveData' => false, + ], + 'Hello John!', + true, + ], + ]; + } + + public function testInvalidInput(): void + { + $this->transformer->configure(['template' => 'test']); + $result = $this->transformer->process('not an array'); + + $this->assertSame('not an array', $result); + $this->assertFalse($this->transformer->isValid()); + } + + public function testNoTemplateConfigured(): void + { + $input = ['test' => 'value']; + $result = $this->transformer->process($input); + + $this->assertSame($input, $result); + $this->assertFalse($this->transformer->isValid()); + } +} diff --git a/tests/Result/TransformationResultTest.php b/tests/Result/TransformationResultTest.php new file mode 100644 index 0000000..824d76b --- /dev/null +++ b/tests/Result/TransformationResultTest.php @@ -0,0 +1,80 @@ +processingResults = $this->createMock(ProcessingResultCollection::class); + } + + /** + * @dataProvider transformationResultProvider + */ + public function testTransformationResult(array $processedData, array $errors, bool $isValid): void + { + $this->processingResults->method('hasErrors')->willReturn(!$isValid); + $this->processingResults->method('getErrors')->willReturn($errors); + $this->processingResults->method('getProcessedData')->willReturn($processedData); + $this->processingResults->method('toArray')->willReturn([ + 'data' => $processedData, + 'errors' => $errors, + ]); + + $result = new TransformationResult($this->processingResults); + + $this->assertSame($isValid, $result->isValid()); + $this->assertSame($errors, $result->getErrors()); + $this->assertSame($processedData, $result->getTransformedData()); + $this->assertEquals([ + 'data' => $processedData, + 'errors' => $errors, + ], $result->toArray()); + } + + public static function transformationResultProvider(): array + { + return [ + 'successful transformation' => [ + ['field1' => 'value1', 'field2' => 'value2'], + [], + true, + ], + 'transformation with multiple errors' => [ + ['field1' => 'value1'], + [ + 'field1' => ['error' => 'Invalid format'], + 'field2' => ['error' => 'Required field'], + ], + false, + ], + 'empty data with no errors' => [ + [], + [], + true, + ], + 'complex nested data' => [ + [ + 'user' => [ + 'profile' => [ + 'name' => 'John', + 'age' => 30, + ], + ], + ], + [], + true, + ], + ]; + } +} diff --git a/tests/Trait/ArrayTransformerTraitTest.php b/tests/Trait/ArrayTransformerTraitTest.php new file mode 100644 index 0000000..07b05d2 --- /dev/null +++ b/tests/Trait/ArrayTransformerTraitTest.php @@ -0,0 +1,81 @@ +trait = new class { + use ArrayTransformerTrait; + + public function transformKeys(array $array, string $case): array + { + return $this->transformArrayKeys($array, $case); + } + }; + } + + /** + * @dataProvider arrayKeyTransformationProvider + */ + public function testArrayKeyTransformation(array $input, string $case, array $expected): void + { + $result = $this->trait->transformKeys($input, $case); + $this->assertSame($expected, $result); + } + + public static function arrayKeyTransformationProvider(): array + { + return [ + 'camelCase keys' => [ + 'input' => ['hello_world' => 1, 'test_value' => 2], + 'case' => 'camel', + 'expected' => ['helloWorld' => 1, 'testValue' => 2], + ], + 'PascalCase keys' => [ + 'input' => ['hello_world' => 1, 'test_value' => 2], + 'case' => 'pascal', + 'expected' => ['HelloWorld' => 1, 'TestValue' => 2], + ], + 'snake_case keys' => [ + 'input' => ['helloWorld' => 1, 'TestValue' => 2], + 'case' => 'snake', + 'expected' => ['hello_world' => 1, 'test_value' => 2], + ], + 'kebab-case keys' => [ + 'input' => ['helloWorld' => 1, 'TestValue' => 2], + 'case' => 'kebab', + 'expected' => ['hello-world' => 1, 'test-value' => 2], + ], + 'nested camelCase keys' => [ + 'input' => ['nested_key' => ['inner_key_value' => 3]], + 'case' => 'camel', + 'expected' => ['nestedKey' => ['innerKeyValue' => 3]], + ], + 'nested PascalCase keys' => [ + 'input' => ['nested_key' => ['inner_key_value' => 3]], + 'case' => 'pascal', + 'expected' => ['NestedKey' => ['InnerKeyValue' => 3]], + ], + 'nested snake_case keys' => [ + 'input' => ['nestedKey' => ['innerKeyValue' => 3]], + 'case' => 'snake', + 'expected' => ['nested_key' => ['inner_key_value' => 3]], + ], + 'nested kebab-case keys' => [ + 'input' => ['nestedKey' => ['innerKeyValue' => 3]], + 'case' => 'kebab', + 'expected' => ['nested-key' => ['inner-key-value' => 3]], + ], + ]; + } +} diff --git a/tests/Trait/StringTransformerTraitTest.php b/tests/Trait/StringTransformerTraitTest.php new file mode 100644 index 0000000..31e75ce --- /dev/null +++ b/tests/Trait/StringTransformerTraitTest.php @@ -0,0 +1,304 @@ +trait = new class { + use StringTransformerTrait; + + public function callToLowerCase(string $input): string + { + return $this->toLowerCase($input); + } + + public function callToUpperCase(string $input): string + { + return $this->toUpperCase($input); + } + + public function callToTitleCase(string $input): string + { + return $this->toTitleCase($input); + } + + public function callToSentenceCase(string $input): string + { + return $this->toSentenceCase($input); + } + + public function callToCamelCase(string $input): string + { + return $this->toCamelCase($input); + } + + public function callToPascalCase(string $input): string + { + return $this->toPascalCase($input); + } + + public function callToSnakeCase(string $input): string + { + return $this->toSnakeCase($input); + } + + public function callToKebabCase(string $input): string + { + return $this->toKebabCase($input); + } + }; + } + + /** + * @dataProvider lowerCaseProvider + */ + public function testToLowerCase(string $input, string $expected): void + { + $result = $this->trait->callToLowerCase($input); + $this->assertEquals($expected, $result); + } + + public static function lowerCaseProvider(): array + { + return [ + 'already lowercase' => ['hello world', 'hello world'], + 'mixed case' => ['Hello World', 'hello world'], + 'uppercase' => ['HELLO WORLD', 'hello world'], + 'with numbers' => ['Hello123World', 'hello123world'], + 'with special chars' => ['Héllö Wörld', 'héllö wörld'], + 'with symbols' => ['Hello@World!', 'hello@world!'], + 'single character' => ['A', 'a'], + 'empty string' => ['', ''], + ]; + } + + /** + * @dataProvider upperCaseProvider + */ + public function testToUpperCase(string $input, string $expected): void + { + $result = $this->trait->callToUpperCase($input); + $this->assertEquals($expected, $result); + } + + public static function upperCaseProvider(): array + { + return [ + 'already uppercase' => ['HELLO WORLD', 'HELLO WORLD'], + 'mixed case' => ['Hello World', 'HELLO WORLD'], + 'lowercase' => ['hello world', 'HELLO WORLD'], + 'with numbers' => ['hello123world', 'HELLO123WORLD'], + 'with special chars' => ['héllö wörld', 'HÉLLÖ WÖRLD'], + 'with symbols' => ['hello@world!', 'HELLO@WORLD!'], + 'single character' => ['a', 'A'], + 'empty string' => ['', ''], + ]; + } + + /** + * @dataProvider titleCaseProvider + */ + public function testToTitleCase(string $input, string $expected): void + { + $result = $this->trait->callToTitleCase($input); + $this->assertEquals($expected, $result); + } + + public static function titleCaseProvider(): array + { + return [ + 'already title case' => ['Hello World', 'Hello World'], + 'lowercase' => ['hello world', 'Hello World'], + 'uppercase' => ['HELLO WORLD', 'Hello World'], + 'multiple words' => ['hello beautiful world', 'Hello Beautiful World'], + 'with numbers' => ['hello 123 world', 'Hello 123 World'], + 'with special chars' => ['héllö wörld', 'Héllö Wörld'], + 'with symbols' => ['hello@world', 'Hello@World'], + 'single word' => ['hello', 'Hello'], + 'empty string' => ['', ''], + ]; + } + + /** + * @dataProvider sentenceCaseProvider + */ + public function testToSentenceCase(string $input, string $expected): void + { + $result = $this->trait->callToSentenceCase($input); + $this->assertEquals($expected, $result); + } + + public static function sentenceCaseProvider(): array + { + return [ + 'already sentence case' => ['Hello world', 'Hello world'], + 'lowercase' => ['hello world', 'Hello world'], + 'uppercase' => ['HELLO WORLD', 'Hello world'], + 'multiple sentences' => ['hello world. goodbye world', 'Hello world. goodbye world'], + 'with numbers' => ['hello 123 world', 'Hello 123 world'], + 'with special chars' => ['héllö wörld', 'Héllö wörld'], + 'single word' => ['hello', 'Hello'], + 'empty string' => ['', ''], + ]; + } + + /** + * @dataProvider camelCaseProvider + */ + public function testToCamelCase(string $input, string $expected): void + { + $result = $this->trait->callToCamelCase($input); + $this->assertEquals($expected, $result); + } + + public static function camelCaseProvider(): array + { + return [ + 'from snake case' => ['hello_world', 'helloWorld'], + 'from kebab case' => ['hello-world', 'helloWorld'], + 'from space separated' => ['hello world', 'helloWorld'], + 'already camel case' => ['helloWorld', 'helloWorld'], + 'from pascal case' => ['HelloWorld', 'helloWorld'], + 'multiple words' => ['hello_beautiful_world', 'helloBeautifulWorld'], + 'with numbers' => ['hello_123_world', 'hello123World'], + 'multiple delimiters' => ['hello-beautiful_world', 'helloBeautifulWorld'], + 'consecutive delimiters' => ['hello__world', 'helloWorld'], + 'empty string' => ['', ''], + ]; + } + + /** + * @dataProvider pascalCaseProvider + */ + public function testToPascalCase(string $input, string $expected): void + { + $result = $this->trait->callToPascalCase($input); + $this->assertEquals($expected, $result); + } + + public static function pascalCaseProvider(): array + { + return [ + 'from snake case' => ['hello_world', 'HelloWorld'], + 'from kebab case' => ['hello-world', 'HelloWorld'], + 'from space separated' => ['hello world', 'HelloWorld'], + 'from camel case' => ['helloWorld', 'HelloWorld'], + 'already pascal case' => ['HelloWorld', 'HelloWorld'], + 'multiple words' => ['hello_beautiful_world', 'HelloBeautifulWorld'], + 'with numbers' => ['hello_123_world', 'Hello123World'], + 'multiple delimiters' => ['hello-beautiful_world', 'HelloBeautifulWorld'], + 'consecutive delimiters' => ['hello__world', 'HelloWorld'], + 'empty string' => ['', ''], + ]; + } + + /** + * @dataProvider snakeCaseProvider + */ + public function testToSnakeCase(string $input, string $expected): void + { + $result = $this->trait->callToSnakeCase($input); + $this->assertEquals($expected, $result); + } + + public static function snakeCaseProvider(): array + { + return [ + 'from camel case' => ['helloWorld', 'hello_world'], + 'from pascal case' => ['HelloWorld', 'hello_world'], + 'from kebab case' => ['hello-world', 'hello_world'], + 'already snake case' => ['hello_world', 'hello_world'], + 'multiple words' => ['helloBeautifulWorld', 'hello_beautiful_world'], + 'with numbers' => ['hello123World', 'hello123_world'], + 'from space separated' => ['hello world', 'hello_world'], + 'consecutive capitals' => ['helloWORLD', 'hello_world'], + 'with acronyms' => ['helloWORLDTest', 'hello_world_test'], + 'empty string' => ['', ''], + ]; + } + + /** + * @dataProvider kebabCaseProvider + */ + public function testToKebabCase(string $input, string $expected): void + { + $result = $this->trait->callToKebabCase($input); + $this->assertEquals($expected, $result); + } + + public static function kebabCaseProvider(): array + { + return [ + 'from camel case' => ['helloWorld', 'hello-world'], + 'from pascal case' => ['HelloWorld', 'hello-world'], + 'from snake case' => ['hello_world', 'hello-world'], + 'already kebab case' => ['hello-world', 'hello-world'], + 'multiple words' => ['helloBeautifulWorld', 'hello-beautiful-world'], + 'with numbers' => ['hello123World', 'hello123-world'], + 'from space separated' => ['hello world', 'hello-world'], + 'consecutive capitals' => ['helloWORLD', 'hello-world'], + 'with acronyms' => ['helloWORLDTest', 'hello-world-test'], + 'empty string' => ['', ''], + ]; + } + + /** + * @dataProvider multiByteProvider + */ + public function testMultiByteStringHandling(string $method, string $input, string $expected): void + { + $methodName = 'callTo' . ucfirst($method); + $result = $this->trait->$methodName($input); + $this->assertEquals($expected, $result); + } + + public static function multiByteProvider(): array + { + return [ + 'toLowerCase with accents' => ['lowerCase', 'CAFÉ', 'café'], + 'toUpperCase with accents' => ['upperCase', 'café', 'CAFÉ'], + 'toTitleCase with accents' => ['titleCase', 'café au lait', 'Café Au Lait'], + 'toSentenceCase with accents' => ['sentenceCase', 'café au lait', 'Café au lait'], + 'toCamelCase with accents' => ['camelCase', 'café_au_lait', 'cafeAuLait'], + 'toPascalCase with accents' => ['pascalCase', 'café_au_lait', 'CafeAuLait'], + 'toSnakeCase with accents' => ['snakeCase', 'caféAuLait', 'cafe_au_lait'], + 'toKebabCase with accents' => ['kebabCase', 'caféAuLait', 'cafe-au-lait'], + ]; + } + + /** + * @dataProvider edgeCasesProvider + */ + public function testEdgeCases(string $method, string $input, string $expected): void + { + $methodName = 'callTo' . ucfirst($method); + $result = $this->trait->$methodName($input); + $this->assertEquals($expected, $result); + } + + public static function edgeCasesProvider(): array + { + return [ + 'empty string to lower' => ['lowerCase', '', ''], + 'empty string to upper' => ['upperCase', '', ''], + 'empty string to title' => ['titleCase', '', ''], + 'empty string to sentence' => ['sentenceCase', '', ''], + 'empty string to camel' => ['camelCase', '', ''], + 'empty string to pascal' => ['pascalCase', '', ''], + 'empty string to snake' => ['snakeCase', '', ''], + 'empty string to kebab' => ['kebabCase', '', ''], + 'single char to camel' => ['camelCase', 'a', 'a'], + 'single char to pascal' => ['pascalCase', 'a', 'A'], + 'multiple spaces' => ['camelCase', 'hello world', 'helloWorld'], + ]; + } +} diff --git a/tests/TransformerTest.php b/tests/TransformerTest.php new file mode 100644 index 0000000..ccdb53b --- /dev/null +++ b/tests/TransformerTest.php @@ -0,0 +1,75 @@ +registry = $this->createMock(ProcessorRegistry::class); + $this->transformer = new Transformer($this->registry); + } + + public function testTransformSimpleObject(): void + { + $object = new class { + #[Transform(processors: ['processor' => ['option' => 'value']])] + public ?string $property = 'test'; + }; + + $result = $this->transformer->transform($object); + + $this->assertInstanceOf(TransformationResult::class, $result); + } + + public function testTransformObjectWithoutAttributes(): void + { + $object = new class { + public ?string $property = 'test'; + }; + + $result = $this->transformer->transform($object); + + $this->assertInstanceOf(TransformationResult::class, $result); + $this->assertTrue($result->isValid()); + $this->assertEmpty($result->getErrors()); + } + + public function testTransformObjectWithMultipleAttributes(): void + { + $object = new class { + #[Transform(processors: ['processor1' => []])] + public ?string $property1 = 'test1'; + + #[Transform(processors: ['processor2' => []])] + public ?string $property2 = 'test2'; + }; + + $result = $this->transformer->transform($object); + + $this->assertInstanceOf(TransformationResult::class, $result); + } + + public function testTransformObjectWithInvalidProcessor(): void + { + $object = new class { + #[Transform(processors: ['invalid_processor' => []])] + public ?string $property = 'test'; + }; + + $result = $this->transformer->transform($object); + + $this->assertInstanceOf(TransformationResult::class, $result); + } +}