From 17f2d4fbcebdc629a2f9c6b16ed9d8339d254200 Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 11:55:52 +0300 Subject: [PATCH 01/14] CONV-046: Create MVP converter capability list --- config/converters.php | 25 +++++++++++++++++++ .../MvpConverterCapabilityListTest.php | 22 ++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 config/converters.php create mode 100644 tests/Unit/Converters/MvpConverterCapabilityListTest.php diff --git a/config/converters.php b/config/converters.php new file mode 100644 index 0000000..2be5053 --- /dev/null +++ b/config/converters.php @@ -0,0 +1,25 @@ + [ + 'png:jpg', + 'png:webp', + 'png:pdf', + 'jpg:png', + 'jpg:webp', + 'jpg:pdf', + ], + +]; diff --git a/tests/Unit/Converters/MvpConverterCapabilityListTest.php b/tests/Unit/Converters/MvpConverterCapabilityListTest.php new file mode 100644 index 0000000..c0f84d6 --- /dev/null +++ b/tests/Unit/Converters/MvpConverterCapabilityListTest.php @@ -0,0 +1,22 @@ +toBeArray(); + expect($capabilities)->toContain('png:jpg'); + expect($capabilities)->toContain('png:webp'); + expect($capabilities)->toContain('png:pdf'); + expect($capabilities)->toContain('jpg:png'); + expect($capabilities)->toContain('jpg:webp'); + expect($capabilities)->toContain('jpg:pdf'); + expect($capabilities)->toHaveCount(6); +}); + +it('does not include non mvp directions in capability list', function () { + $capabilities = config('converters.mvp_capabilities'); + + expect($capabilities)->not->toContain('mp4:mp3'); + expect($capabilities)->not->toContain('pdf:docx'); + expect($capabilities)->not->toContain('docx:pdf'); +}); From 3cb257756e6ffff297151a77a1c2ed83a896c098 Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 13:21:30 +0300 Subject: [PATCH 02/14] CONV-047: Test PNG target capabilities --- .../Converters/ConverterRegistryTargetsTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/Unit/Converters/ConverterRegistryTargetsTest.php diff --git a/tests/Unit/Converters/ConverterRegistryTargetsTest.php b/tests/Unit/Converters/ConverterRegistryTargetsTest.php new file mode 100644 index 0000000..60f2e3f --- /dev/null +++ b/tests/Unit/Converters/ConverterRegistryTargetsTest.php @@ -0,0 +1,16 @@ +targetsFor('png')) + ->map(fn ($target) => $target->format) + ->all(); + + expect($targets)->toContain('jpg'); + expect($targets)->toContain('webp'); + expect($targets)->toContain('pdf'); + expect($targets)->not->toContain('mp3'); +})->skip('CONV-056 wires the registry binding; this test will become live then.'); From 11d371adf24558ee82966cf9d445809d9312e4d4 Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 13:39:04 +0300 Subject: [PATCH 03/14] CONV-048: Add PNG to JPG converter capability --- .../Converters/Image/PngToJpgConverter.php | 84 +++++++++++++++++++ .../Image/PngToJpgConverterTest.php | 30 +++++++ 2 files changed, 114 insertions(+) create mode 100644 app/Support/Converters/Image/PngToJpgConverter.php create mode 100644 tests/Unit/Converters/Image/PngToJpgConverterTest.php diff --git a/app/Support/Converters/Image/PngToJpgConverter.php b/app/Support/Converters/Image/PngToJpgConverter.php new file mode 100644 index 0000000..580e3ad --- /dev/null +++ b/app/Support/Converters/Image/PngToJpgConverter.php @@ -0,0 +1,84 @@ + 'quality', + 'type' => 'segmented', + 'label' => 'Quality', + 'default' => 'high', + 'options' => [ + ['value' => 'medium', 'label' => 'Medium'], + ['value' => 'high', 'label' => 'High'], + ['value' => 'best', 'label' => 'Best'], + ], + ], + [ + 'key' => 'resize', + 'type' => 'select', + 'label' => 'Resize', + 'default' => 'original', + 'options' => [ + ['value' => 'original', 'label' => 'Original'], + ['value' => '1920', 'label' => '1920 px'], + ['value' => '1280', 'label' => '1280 px'], + ['value' => 'custom', 'label' => 'Custom'], + ], + ], + [ + 'key' => 'background_color', + 'type' => 'color', + 'label' => 'Background color', + 'default' => '#ffffff', + ], + [ + 'key' => 'remove_metadata', + 'type' => 'toggle', + 'label' => 'Remove metadata', + 'default' => true, + ], + ]; + } + + public function validateOptions(array $options): array + { + return $this->optionsValidator->validate($this->optionsSchema(), $options); + } +} diff --git a/tests/Unit/Converters/Image/PngToJpgConverterTest.php b/tests/Unit/Converters/Image/PngToJpgConverterTest.php new file mode 100644 index 0000000..2edcc5b --- /dev/null +++ b/tests/Unit/Converters/Image/PngToJpgConverterTest.php @@ -0,0 +1,30 @@ +key())->toBe('png:jpg'); + expect($converter->sourceFormat())->toBe('png'); + expect($converter->targetFormat())->toBe('jpg'); + expect($converter->label())->toBe('JPG'); + expect($converter->description())->not->toBeEmpty(); +}); + +it('provides valid default options for png to jpg', function () { + $converter = app(PngToJpgConverter::class); + + $options = app(OptionsValidator::class)->validate( + $converter->optionsSchema(), + [] + ); + + expect($options)->toHaveKey('quality'); + expect($options)->toHaveKey('resize'); + expect($options)->toHaveKey('background_color'); + expect($options)->toHaveKey('remove_metadata'); + expect($options['background_color'])->toBe('#ffffff'); + expect($options['remove_metadata'])->toBeTrue(); +}); From edab1e245c3ae4a2b8c322774d281046b8bfbcea Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 14:12:26 +0300 Subject: [PATCH 04/14] CONV-049: Add PNG to WEBP converter capability --- .../Converters/Image/PngToWebpConverter.php | 84 +++++++++++++++++++ .../Image/PngToWebpConverterTest.php | 29 +++++++ 2 files changed, 113 insertions(+) create mode 100644 app/Support/Converters/Image/PngToWebpConverter.php create mode 100644 tests/Unit/Converters/Image/PngToWebpConverterTest.php diff --git a/app/Support/Converters/Image/PngToWebpConverter.php b/app/Support/Converters/Image/PngToWebpConverter.php new file mode 100644 index 0000000..39a23e5 --- /dev/null +++ b/app/Support/Converters/Image/PngToWebpConverter.php @@ -0,0 +1,84 @@ + 'quality', + 'type' => 'segmented', + 'label' => 'Quality', + 'default' => 'high', + 'options' => [ + ['value' => 'medium', 'label' => 'Medium'], + ['value' => 'high', 'label' => 'High'], + ['value' => 'best', 'label' => 'Best'], + ], + ], + [ + 'key' => 'lossless', + 'type' => 'toggle', + 'label' => 'Lossless', + 'default' => false, + ], + [ + 'key' => 'resize', + 'type' => 'select', + 'label' => 'Resize', + 'default' => 'original', + 'options' => [ + ['value' => 'original', 'label' => 'Original'], + ['value' => '1920', 'label' => '1920 px'], + ['value' => '1280', 'label' => '1280 px'], + ['value' => 'custom', 'label' => 'Custom'], + ], + ], + [ + 'key' => 'remove_metadata', + 'type' => 'toggle', + 'label' => 'Remove metadata', + 'default' => true, + ], + ]; + } + + public function validateOptions(array $options): array + { + return $this->optionsValidator->validate($this->optionsSchema(), $options); + } +} diff --git a/tests/Unit/Converters/Image/PngToWebpConverterTest.php b/tests/Unit/Converters/Image/PngToWebpConverterTest.php new file mode 100644 index 0000000..f1df8b6 --- /dev/null +++ b/tests/Unit/Converters/Image/PngToWebpConverterTest.php @@ -0,0 +1,29 @@ +key())->toBe('png:webp'); + expect($converter->sourceFormat())->toBe('png'); + expect($converter->targetFormat())->toBe('webp'); + expect($converter->label())->toBe('WEBP'); + expect($converter->description())->not->toBeEmpty(); +}); + +it('provides valid default options for png to webp', function () { + $converter = app(PngToWebpConverter::class); + + $options = app(OptionsValidator::class)->validate( + $converter->optionsSchema(), + [] + ); + + expect($options)->toHaveKey('quality'); + expect($options)->toHaveKey('lossless'); + expect($options)->toHaveKey('resize'); + expect($options)->toHaveKey('remove_metadata'); + expect($options['lossless'])->toBeFalse(); +}); From bc0d38c609cd8327dec65c35420f9ae09771041e Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 14:13:16 +0300 Subject: [PATCH 05/14] CONV-050: Add PNG to PDF converter capability --- .../Converters/Image/PngToPdfConverter.php | 104 ++++++++++++++++++ .../Image/PngToPdfConverterTest.php | 29 +++++ 2 files changed, 133 insertions(+) create mode 100644 app/Support/Converters/Image/PngToPdfConverter.php create mode 100644 tests/Unit/Converters/Image/PngToPdfConverterTest.php diff --git a/app/Support/Converters/Image/PngToPdfConverter.php b/app/Support/Converters/Image/PngToPdfConverter.php new file mode 100644 index 0000000..4e50e43 --- /dev/null +++ b/app/Support/Converters/Image/PngToPdfConverter.php @@ -0,0 +1,104 @@ + 'page_size', + 'type' => 'segmented', + 'label' => 'Page size', + 'default' => 'auto', + 'options' => [ + ['value' => 'auto', 'label' => 'Auto'], + ['value' => 'a4', 'label' => 'A4'], + ['value' => 'letter', 'label' => 'Letter'], + ], + ], + [ + 'key' => 'orientation', + 'type' => 'segmented', + 'label' => 'Orientation', + 'default' => 'auto', + 'options' => [ + ['value' => 'auto', 'label' => 'Auto'], + ['value' => 'portrait', 'label' => 'Portrait'], + ['value' => 'landscape', 'label' => 'Landscape'], + ], + ], + [ + 'key' => 'margin', + 'type' => 'segmented', + 'label' => 'Margin', + 'default' => 'small', + 'options' => [ + ['value' => 'none', 'label' => 'None'], + ['value' => 'small', 'label' => 'Small'], + ['value' => 'medium', 'label' => 'Medium'], + ], + ], + [ + 'key' => 'fit_mode', + 'type' => 'segmented', + 'label' => 'Fit mode', + 'default' => 'contain', + 'options' => [ + ['value' => 'contain', 'label' => 'Contain'], + ['value' => 'cover', 'label' => 'Cover'], + ['value' => 'original', 'label' => 'Original'], + ], + ], + [ + 'key' => 'compression', + 'type' => 'segmented', + 'label' => 'Compression', + 'default' => 'balanced', + 'options' => [ + ['value' => 'none', 'label' => 'None'], + ['value' => 'balanced', 'label' => 'Balanced'], + ['value' => 'small', 'label' => 'Small'], + ], + ], + ]; + } + + public function validateOptions(array $options): array + { + return $this->optionsValidator->validate($this->optionsSchema(), $options); + } +} diff --git a/tests/Unit/Converters/Image/PngToPdfConverterTest.php b/tests/Unit/Converters/Image/PngToPdfConverterTest.php new file mode 100644 index 0000000..231761a --- /dev/null +++ b/tests/Unit/Converters/Image/PngToPdfConverterTest.php @@ -0,0 +1,29 @@ +key())->toBe('png:pdf'); + expect($converter->sourceFormat())->toBe('png'); + expect($converter->targetFormat())->toBe('pdf'); + expect($converter->label())->toBe('PDF'); + expect($converter->description())->not->toBeEmpty(); +}); + +it('provides valid default options for png to pdf', function () { + $converter = app(PngToPdfConverter::class); + + $options = app(OptionsValidator::class)->validate( + $converter->optionsSchema(), + [] + ); + + expect($options)->toHaveKey('page_size'); + expect($options)->toHaveKey('orientation'); + expect($options)->toHaveKey('margin'); + expect($options)->toHaveKey('fit_mode'); + expect($options)->toHaveKey('compression'); +}); From f29cbd62cc177fe6b52de8737590850b3bd60773 Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 14:15:31 +0300 Subject: [PATCH 06/14] CONV-051: Test JPG target capabilities --- .../Converters/ConverterRegistryTargetsTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Unit/Converters/ConverterRegistryTargetsTest.php b/tests/Unit/Converters/ConverterRegistryTargetsTest.php index 60f2e3f..c2bddc5 100644 --- a/tests/Unit/Converters/ConverterRegistryTargetsTest.php +++ b/tests/Unit/Converters/ConverterRegistryTargetsTest.php @@ -14,3 +14,16 @@ expect($targets)->toContain('pdf'); expect($targets)->not->toContain('mp3'); })->skip('CONV-056 wires the registry binding; this test will become live then.'); + +it('lists jpg target capabilities', function () { + $registry = app(ConverterRegistry::class); + + $targets = collect($registry->targetsFor('jpg')) + ->map(fn ($target) => $target->format) + ->all(); + + expect($targets)->toContain('png'); + expect($targets)->toContain('webp'); + expect($targets)->toContain('pdf'); + expect($targets)->not->toContain('mp3'); +})->skip('CONV-056 wires the registry binding; this test will become live then.'); From d62cef01a526dec16ab38d0cfb3b9c60ab6dae47 Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 14:32:26 +0300 Subject: [PATCH 07/14] CONV-052: Add JPG to PNG converter capability --- .../Converters/Image/JpgToPngConverter.php | 67 +++++++++++++++++++ .../Image/JpgToPngConverterTest.php | 27 ++++++++ 2 files changed, 94 insertions(+) create mode 100644 app/Support/Converters/Image/JpgToPngConverter.php create mode 100644 tests/Unit/Converters/Image/JpgToPngConverterTest.php diff --git a/app/Support/Converters/Image/JpgToPngConverter.php b/app/Support/Converters/Image/JpgToPngConverter.php new file mode 100644 index 0000000..a597748 --- /dev/null +++ b/app/Support/Converters/Image/JpgToPngConverter.php @@ -0,0 +1,67 @@ + 'resize', + 'type' => 'select', + 'label' => 'Resize', + 'default' => 'original', + 'options' => [ + ['value' => 'original', 'label' => 'Original'], + ['value' => '1920', 'label' => '1920 px'], + ['value' => '1280', 'label' => '1280 px'], + ['value' => 'custom', 'label' => 'Custom'], + ], + ], + [ + 'key' => 'remove_metadata', + 'type' => 'toggle', + 'label' => 'Remove metadata', + 'default' => true, + ], + ]; + } + + public function validateOptions(array $options): array + { + return $this->optionsValidator->validate($this->optionsSchema(), $options); + } +} diff --git a/tests/Unit/Converters/Image/JpgToPngConverterTest.php b/tests/Unit/Converters/Image/JpgToPngConverterTest.php new file mode 100644 index 0000000..4a0ec9c --- /dev/null +++ b/tests/Unit/Converters/Image/JpgToPngConverterTest.php @@ -0,0 +1,27 @@ +key())->toBe('jpg:png'); + expect($converter->sourceFormat())->toBe('jpg'); + expect($converter->targetFormat())->toBe('png'); + expect($converter->label())->toBe('PNG'); + expect($converter->description())->not->toBeEmpty(); +}); + +it('provides valid default options for jpg to png', function () { + $converter = app(JpgToPngConverter::class); + + $options = app(OptionsValidator::class)->validate( + $converter->optionsSchema(), + [] + ); + + expect($options)->toHaveKey('resize'); + expect($options)->toHaveKey('remove_metadata'); + expect($options)->not->toHaveKey('transparency'); +}); From c308bb65beee88f657e769fbf36eece3a9afd619 Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 14:44:05 +0300 Subject: [PATCH 08/14] CONV-053: Add JPG to WEBP converter capability --- .../Converters/Image/JpgToWebpConverter.php | 78 +++++++++++++++++++ .../Image/JpgToWebpConverterTest.php | 26 +++++++ 2 files changed, 104 insertions(+) create mode 100644 app/Support/Converters/Image/JpgToWebpConverter.php create mode 100644 tests/Unit/Converters/Image/JpgToWebpConverterTest.php diff --git a/app/Support/Converters/Image/JpgToWebpConverter.php b/app/Support/Converters/Image/JpgToWebpConverter.php new file mode 100644 index 0000000..59dec70 --- /dev/null +++ b/app/Support/Converters/Image/JpgToWebpConverter.php @@ -0,0 +1,78 @@ + 'quality', + 'type' => 'segmented', + 'label' => 'Quality', + 'default' => 'high', + 'options' => [ + ['value' => 'medium', 'label' => 'Medium'], + ['value' => 'high', 'label' => 'High'], + ['value' => 'best', 'label' => 'Best'], + ], + ], + [ + 'key' => 'resize', + 'type' => 'select', + 'label' => 'Resize', + 'default' => 'original', + 'options' => [ + ['value' => 'original', 'label' => 'Original'], + ['value' => '1920', 'label' => '1920 px'], + ['value' => '1280', 'label' => '1280 px'], + ['value' => 'custom', 'label' => 'Custom'], + ], + ], + [ + 'key' => 'remove_metadata', + 'type' => 'toggle', + 'label' => 'Remove metadata', + 'default' => true, + ], + ]; + } + + public function validateOptions(array $options): array + { + return $this->optionsValidator->validate($this->optionsSchema(), $options); + } +} diff --git a/tests/Unit/Converters/Image/JpgToWebpConverterTest.php b/tests/Unit/Converters/Image/JpgToWebpConverterTest.php new file mode 100644 index 0000000..8e40eb2 --- /dev/null +++ b/tests/Unit/Converters/Image/JpgToWebpConverterTest.php @@ -0,0 +1,26 @@ +key())->toBe('jpg:webp'); + expect($converter->sourceFormat())->toBe('jpg'); + expect($converter->targetFormat())->toBe('webp'); + expect($converter->label())->toBe('WEBP'); +}); + +it('provides valid default options for jpg to webp', function () { + $converter = app(JpgToWebpConverter::class); + + $options = app(OptionsValidator::class)->validate( + $converter->optionsSchema(), + [] + ); + + expect($options)->toHaveKey('quality'); + expect($options)->toHaveKey('resize'); + expect($options)->toHaveKey('remove_metadata'); +}); From 4867f036fb9e0673b7f7e068980c5a781120138c Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 14:52:33 +0300 Subject: [PATCH 09/14] CONV-054: Add JPG to PDF converter capability --- .../Converters/Image/JpgToPdfConverter.php | 104 ++++++++++++++++++ .../Image/JpgToPdfConverterTest.php | 28 +++++ 2 files changed, 132 insertions(+) create mode 100644 app/Support/Converters/Image/JpgToPdfConverter.php create mode 100644 tests/Unit/Converters/Image/JpgToPdfConverterTest.php diff --git a/app/Support/Converters/Image/JpgToPdfConverter.php b/app/Support/Converters/Image/JpgToPdfConverter.php new file mode 100644 index 0000000..272b260 --- /dev/null +++ b/app/Support/Converters/Image/JpgToPdfConverter.php @@ -0,0 +1,104 @@ + 'page_size', + 'type' => 'segmented', + 'label' => 'Page size', + 'default' => 'auto', + 'options' => [ + ['value' => 'auto', 'label' => 'Auto'], + ['value' => 'a4', 'label' => 'A4'], + ['value' => 'letter', 'label' => 'Letter'], + ], + ], + [ + 'key' => 'orientation', + 'type' => 'segmented', + 'label' => 'Orientation', + 'default' => 'auto', + 'options' => [ + ['value' => 'auto', 'label' => 'Auto'], + ['value' => 'portrait', 'label' => 'Portrait'], + ['value' => 'landscape', 'label' => 'Landscape'], + ], + ], + [ + 'key' => 'margin', + 'type' => 'segmented', + 'label' => 'Margin', + 'default' => 'small', + 'options' => [ + ['value' => 'none', 'label' => 'None'], + ['value' => 'small', 'label' => 'Small'], + ['value' => 'medium', 'label' => 'Medium'], + ], + ], + [ + 'key' => 'fit_mode', + 'type' => 'segmented', + 'label' => 'Fit mode', + 'default' => 'contain', + 'options' => [ + ['value' => 'contain', 'label' => 'Contain'], + ['value' => 'cover', 'label' => 'Cover'], + ['value' => 'original', 'label' => 'Original'], + ], + ], + [ + 'key' => 'compression', + 'type' => 'segmented', + 'label' => 'Compression', + 'default' => 'balanced', + 'options' => [ + ['value' => 'none', 'label' => 'None'], + ['value' => 'balanced', 'label' => 'Balanced'], + ['value' => 'small', 'label' => 'Small'], + ], + ], + ]; + } + + public function validateOptions(array $options): array + { + return $this->optionsValidator->validate($this->optionsSchema(), $options); + } +} diff --git a/tests/Unit/Converters/Image/JpgToPdfConverterTest.php b/tests/Unit/Converters/Image/JpgToPdfConverterTest.php new file mode 100644 index 0000000..689ceaf --- /dev/null +++ b/tests/Unit/Converters/Image/JpgToPdfConverterTest.php @@ -0,0 +1,28 @@ +key())->toBe('jpg:pdf'); + expect($converter->sourceFormat())->toBe('jpg'); + expect($converter->targetFormat())->toBe('pdf'); + expect($converter->label())->toBe('PDF'); +}); + +it('provides valid default options for jpg to pdf', function () { + $converter = app(JpgToPdfConverter::class); + + $options = app(OptionsValidator::class)->validate( + $converter->optionsSchema(), + [] + ); + + expect($options)->toHaveKey('page_size'); + expect($options)->toHaveKey('orientation'); + expect($options)->toHaveKey('margin'); + expect($options)->toHaveKey('fit_mode'); + expect($options)->toHaveKey('compression'); +}); From 8cc970692ea709ffb80b167ae36cedf760f6edda Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 15:16:12 +0300 Subject: [PATCH 10/14] CONV-055: Add recommended target metadata --- .../Converters/Contracts/Converter.php | 2 + app/Support/Converters/ConverterRegistry.php | 2 +- .../Converters/Image/JpgToPdfConverter.php | 5 +++ .../Converters/Image/JpgToPngConverter.php | 5 +++ .../Converters/Image/JpgToWebpConverter.php | 5 +++ .../Converters/Image/PngToJpgConverter.php | 5 +++ .../Converters/Image/PngToPdfConverter.php | 5 +++ .../Converters/Image/PngToWebpConverter.php | 5 +++ .../Converters/FakeJpgToWebpConverter.php | 5 +++ .../Converters/FakePngToJpgConverter.php | 5 +++ .../Converters/FakePngToPdfConverter.php | 5 +++ .../RecommendedTargetMetadataTest.php | 37 +++++++++++++++++++ 12 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Converters/RecommendedTargetMetadataTest.php diff --git a/app/Support/Converters/Contracts/Converter.php b/app/Support/Converters/Contracts/Converter.php index 93e0529..d7094c2 100644 --- a/app/Support/Converters/Contracts/Converter.php +++ b/app/Support/Converters/Contracts/Converter.php @@ -17,4 +17,6 @@ public function description(): string; public function optionsSchema(): array; public function validateOptions(array $options): array; + + public function isRecommended(): bool; } diff --git a/app/Support/Converters/ConverterRegistry.php b/app/Support/Converters/ConverterRegistry.php index 453da71..7242ed7 100644 --- a/app/Support/Converters/ConverterRegistry.php +++ b/app/Support/Converters/ConverterRegistry.php @@ -53,7 +53,7 @@ public function targetsFor(string $source): array label: $converter->label(), description: $converter->description(), converterKey: $converter->key(), - recommended: false, + recommended: $converter->isRecommended(), ), $this->forSource($source), ); diff --git a/app/Support/Converters/Image/JpgToPdfConverter.php b/app/Support/Converters/Image/JpgToPdfConverter.php index 272b260..a75d14b 100644 --- a/app/Support/Converters/Image/JpgToPdfConverter.php +++ b/app/Support/Converters/Image/JpgToPdfConverter.php @@ -101,4 +101,9 @@ public function validateOptions(array $options): array { return $this->optionsValidator->validate($this->optionsSchema(), $options); } + + public function isRecommended(): bool + { + return false; + } } diff --git a/app/Support/Converters/Image/JpgToPngConverter.php b/app/Support/Converters/Image/JpgToPngConverter.php index a597748..d37b9a4 100644 --- a/app/Support/Converters/Image/JpgToPngConverter.php +++ b/app/Support/Converters/Image/JpgToPngConverter.php @@ -64,4 +64,9 @@ public function validateOptions(array $options): array { return $this->optionsValidator->validate($this->optionsSchema(), $options); } + + public function isRecommended(): bool + { + return false; + } } diff --git a/app/Support/Converters/Image/JpgToWebpConverter.php b/app/Support/Converters/Image/JpgToWebpConverter.php index 59dec70..500030f 100644 --- a/app/Support/Converters/Image/JpgToWebpConverter.php +++ b/app/Support/Converters/Image/JpgToWebpConverter.php @@ -75,4 +75,9 @@ public function validateOptions(array $options): array { return $this->optionsValidator->validate($this->optionsSchema(), $options); } + + public function isRecommended(): bool + { + return true; + } } diff --git a/app/Support/Converters/Image/PngToJpgConverter.php b/app/Support/Converters/Image/PngToJpgConverter.php index 580e3ad..d197ae5 100644 --- a/app/Support/Converters/Image/PngToJpgConverter.php +++ b/app/Support/Converters/Image/PngToJpgConverter.php @@ -81,4 +81,9 @@ public function validateOptions(array $options): array { return $this->optionsValidator->validate($this->optionsSchema(), $options); } + + public function isRecommended(): bool + { + return true; + } } diff --git a/app/Support/Converters/Image/PngToPdfConverter.php b/app/Support/Converters/Image/PngToPdfConverter.php index 4e50e43..a0bdd95 100644 --- a/app/Support/Converters/Image/PngToPdfConverter.php +++ b/app/Support/Converters/Image/PngToPdfConverter.php @@ -101,4 +101,9 @@ public function validateOptions(array $options): array { return $this->optionsValidator->validate($this->optionsSchema(), $options); } + + public function isRecommended(): bool + { + return false; + } } diff --git a/app/Support/Converters/Image/PngToWebpConverter.php b/app/Support/Converters/Image/PngToWebpConverter.php index 39a23e5..79c5a1c 100644 --- a/app/Support/Converters/Image/PngToWebpConverter.php +++ b/app/Support/Converters/Image/PngToWebpConverter.php @@ -81,4 +81,9 @@ public function validateOptions(array $options): array { return $this->optionsValidator->validate($this->optionsSchema(), $options); } + + public function isRecommended(): bool + { + return false; + } } diff --git a/tests/Fakes/Converters/FakeJpgToWebpConverter.php b/tests/Fakes/Converters/FakeJpgToWebpConverter.php index 0b3356c..469b86f 100644 --- a/tests/Fakes/Converters/FakeJpgToWebpConverter.php +++ b/tests/Fakes/Converters/FakeJpgToWebpConverter.php @@ -40,4 +40,9 @@ public function validateOptions(array $options): array { return $options; } + + public function isRecommended(): bool + { + return false; + } } diff --git a/tests/Fakes/Converters/FakePngToJpgConverter.php b/tests/Fakes/Converters/FakePngToJpgConverter.php index c667bba..21c53c8 100644 --- a/tests/Fakes/Converters/FakePngToJpgConverter.php +++ b/tests/Fakes/Converters/FakePngToJpgConverter.php @@ -40,4 +40,9 @@ public function validateOptions(array $options): array { return $options; } + + public function isRecommended(): bool + { + return false; + } } diff --git a/tests/Fakes/Converters/FakePngToPdfConverter.php b/tests/Fakes/Converters/FakePngToPdfConverter.php index 81bff9b..01a33cd 100644 --- a/tests/Fakes/Converters/FakePngToPdfConverter.php +++ b/tests/Fakes/Converters/FakePngToPdfConverter.php @@ -40,4 +40,9 @@ public function validateOptions(array $options): array { return $options; } + + public function isRecommended(): bool + { + return false; + } } diff --git a/tests/Unit/Converters/RecommendedTargetMetadataTest.php b/tests/Unit/Converters/RecommendedTargetMetadataTest.php new file mode 100644 index 0000000..02f5997 --- /dev/null +++ b/tests/Unit/Converters/RecommendedTargetMetadataTest.php @@ -0,0 +1,37 @@ +targetsFor('png')) + ->filter(fn ($target) => $target->recommended === true); + + expect($recommended)->toHaveCount(1); + expect($recommended->first()->format)->toBe('jpg'); +}); + +it('marks one jpg target as recommended', function () { + $recommended = collect(mvpRegistry()->targetsFor('jpg')) + ->filter(fn ($target) => $target->recommended === true); + + expect($recommended)->toHaveCount(1); + expect($recommended->first()->format)->toBe('webp'); +}); From e6f640e645e57611cbbad9deebdf0a6ee9c68032 Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 15:18:11 +0300 Subject: [PATCH 11/14] CONV-056: Register MVP converter capabilities --- app/Providers/ConverterServiceProvider.php | 29 +++++++++++++++++++ app/Support/Converters/ConverterRegistry.php | 15 ++++++++-- bootstrap/providers.php | 2 ++ .../ConverterRegistryTargetsTest.php | 28 ++++++++++++++++-- 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 app/Providers/ConverterServiceProvider.php diff --git a/app/Providers/ConverterServiceProvider.php b/app/Providers/ConverterServiceProvider.php new file mode 100644 index 0000000..42feda6 --- /dev/null +++ b/app/Providers/ConverterServiceProvider.php @@ -0,0 +1,29 @@ +app->singleton(ConverterRegistry::class, function ($app) { + return new ConverterRegistry([ + $app->make(PngToJpgConverter::class), + $app->make(PngToWebpConverter::class), + $app->make(PngToPdfConverter::class), + $app->make(JpgToPngConverter::class), + $app->make(JpgToWebpConverter::class), + $app->make(JpgToPdfConverter::class), + ]); + }); + } +} diff --git a/app/Support/Converters/ConverterRegistry.php b/app/Support/Converters/ConverterRegistry.php index 7242ed7..34f288c 100644 --- a/app/Support/Converters/ConverterRegistry.php +++ b/app/Support/Converters/ConverterRegistry.php @@ -5,6 +5,7 @@ use App\Enums\FileFormat; use App\Support\Converters\Contracts\Converter; use App\Support\Converters\DTO\ConverterTarget; +use App\Support\Converters\Exceptions\UnsupportedFormatException; final class ConverterRegistry { @@ -22,7 +23,11 @@ public function all(): array /** @return list */ public function forSource(string $source): array { - $source = FileFormat::normalize($source); + try { + $source = FileFormat::normalize($source); + } catch (UnsupportedFormatException) { + return []; + } return array_values(array_filter( $this->converters, @@ -32,8 +37,12 @@ public function forSource(string $source): array public function find(string $source, string $target): ?Converter { - $source = FileFormat::normalize($source); - $target = FileFormat::normalize($target); + try { + $source = FileFormat::normalize($source); + $target = FileFormat::normalize($target); + } catch (UnsupportedFormatException) { + return null; + } foreach ($this->converters as $converter) { if ($converter->sourceFormat() === $source && $converter->targetFormat() === $target) { diff --git a/bootstrap/providers.php b/bootstrap/providers.php index fc94ae6..c8c0245 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -1,7 +1,9 @@ toContain('webp'); expect($targets)->toContain('pdf'); expect($targets)->not->toContain('mp3'); -})->skip('CONV-056 wires the registry binding; this test will become live then.'); +}); it('lists jpg target capabilities', function () { $registry = app(ConverterRegistry::class); @@ -26,4 +26,28 @@ expect($targets)->toContain('webp'); expect($targets)->toContain('pdf'); expect($targets)->not->toContain('mp3'); -})->skip('CONV-056 wires the registry binding; this test will become live then.'); +}); + +it('finds exact mvp converter pairs', function () { + $registry = app(ConverterRegistry::class); + + expect($registry->find('png', 'jpg'))->not->toBeNull(); + expect($registry->find('png', 'webp'))->not->toBeNull(); + expect($registry->find('png', 'pdf'))->not->toBeNull(); + expect($registry->find('jpg', 'png'))->not->toBeNull(); + expect($registry->find('jpg', 'webp'))->not->toBeNull(); + expect($registry->find('jpg', 'pdf'))->not->toBeNull(); +}); + +it('does not find unsupported converter pairs', function () { + $registry = app(ConverterRegistry::class); + + expect($registry->find('png', 'mp3'))->toBeNull(); + expect($registry->find('pdf', 'docx'))->toBeNull(); +}); + +it('returns empty target list for unsupported source format', function () { + $registry = app(ConverterRegistry::class); + + expect($registry->targetsFor('mp4'))->toBe([]); +}); From 8900be38d6037a223e8eb6e93231e125466aa22b Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 15:20:21 +0300 Subject: [PATCH 12/14] CONV-057: Add converter catalog smoke tests --- .../MvpConverterCatalogSmokeTest.php | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/Unit/Converters/MvpConverterCatalogSmokeTest.php diff --git a/tests/Unit/Converters/MvpConverterCatalogSmokeTest.php b/tests/Unit/Converters/MvpConverterCatalogSmokeTest.php new file mode 100644 index 0000000..1690f50 --- /dev/null +++ b/tests/Unit/Converters/MvpConverterCatalogSmokeTest.php @@ -0,0 +1,65 @@ +all()) + ->map(fn ($converter) => $converter->key()) + ->sort() + ->values() + ->all(); + + expect($keys)->toBe([ + 'jpg:pdf', + 'jpg:png', + 'jpg:webp', + 'png:jpg', + 'png:pdf', + 'png:webp', + ]); +}); + +it('has valid schemas and default options for every mvp converter', function () { + $registry = app(ConverterRegistry::class); + $schemaValidator = app(OptionsSchemaValidator::class); + $optionsValidator = app(OptionsValidator::class); + + foreach ($registry->all() as $converter) { + $schemaValidator->validate($converter->optionsSchema()); + + $options = $optionsValidator->validate($converter->optionsSchema(), []); + + expect($options)->toBeArray(); + } +}); + +it('exposes a label and description for every mvp converter', function () { + $registry = app(ConverterRegistry::class); + + foreach ($registry->all() as $converter) { + expect($converter->label())->not->toBeEmpty(); + expect($converter->description())->not->toBeEmpty(); + } +}); + +it('mvp capability list matches registered converter keys', function () { + $registry = app(ConverterRegistry::class); + + $registered = collect($registry->all()) + ->map(fn ($converter) => $converter->key()) + ->sort() + ->values() + ->all(); + + $declared = collect(config('converters.mvp_capabilities')) + ->map(fn ($key) => str_replace(':', ':', $key)) + ->sort() + ->values() + ->all(); + + expect($registered)->toBe($declared); +}); From 3630fea7a1d76bc038270cae06977584393d08e0 Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 15:29:35 +0300 Subject: [PATCH 13/14] chore: allow CodeRabbit auto-review on main and develop base branches --- .coderabbit.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 8692e51..23999cf 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -11,6 +11,9 @@ reviews: enabled: true labels: - "release" + base_branches: + - "main" + - "develop" drafts: false request_changes_workflow: false high_level_summary: true From ddc3ba03e0bdde6a7706656aaf85cb875adcb933 Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Fri, 29 May 2026 15:29:58 +0300 Subject: [PATCH 14/14] chore: limit CodeRabbit auto-review to main base branch only --- .coderabbit.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 23999cf..c958a85 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -13,7 +13,6 @@ reviews: - "release" base_branches: - "main" - - "develop" drafts: false request_changes_workflow: false high_level_summary: true