diff --git a/library/Message/Placeholder/Listed.php b/library/Message/Placeholder/Listed.php new file mode 100644 index 000000000..22e683962 --- /dev/null +++ b/library/Message/Placeholder/Listed.php @@ -0,0 +1,20 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +namespace Respect\Validation\Message\Placeholder; + +final class Listed +{ + /** @param array $values */ + public function __construct( + public readonly array $values, + public readonly string $lastGlue + ) { + } +} diff --git a/library/Message/StandardRenderer.php b/library/Message/StandardRenderer.php index 9251fa384..6a2069972 100644 --- a/library/Message/StandardRenderer.php +++ b/library/Message/StandardRenderer.php @@ -11,11 +11,13 @@ use ReflectionClass; use Respect\Stringifier\Stringifier; +use Respect\Validation\Message\Placeholder\Listed; use Respect\Validation\Message\Placeholder\Quoted; use Respect\Validation\Mode; use Respect\Validation\Result; use Respect\Validation\Rule; +use function is_array; use function is_bool; use function is_scalar; use function is_string; @@ -76,6 +78,14 @@ private function placeholder(string $name, mixed $value, Translator $translator, return $this->placeholder($name, new Quoted($value), $translator); } + if ($modifier === 'listOr' && is_array($value)) { + return $this->placeholder($name, new Listed($value, $translator->translate('or')), $translator); + } + + if ($modifier === 'listAnd' && is_array($value)) { + return $this->placeholder($name, new Listed($value, $translator->translate('and')), $translator); + } + if ($modifier === 'raw' && is_scalar($value)) { return is_bool($value) ? (string) (int) $value : (string) $value; } diff --git a/library/Message/StandardStringifier.php b/library/Message/StandardStringifier.php index 8a9f8c50a..3f3d27c49 100644 --- a/library/Message/StandardStringifier.php +++ b/library/Message/StandardStringifier.php @@ -31,6 +31,7 @@ use Respect\Stringifier\Stringifiers\ResourceStringifier; use Respect\Stringifier\Stringifiers\StringableObjectStringifier; use Respect\Stringifier\Stringifiers\ThrowableObjectStringifier; +use Respect\Validation\Message\Stringifier\ListedStringifier; use Respect\Validation\Message\Stringifier\QuotedStringifier; final class StandardStringifier implements Stringifier @@ -88,6 +89,7 @@ private function createStringifier(Quoter $quoter): Stringifier $stringifier->prependStringifier(new DateTimeStringifier($quoter, DateTimeInterface::ATOM)); $stringifier->prependStringifier(new IteratorObjectStringifier($stringifier, $quoter)); $stringifier->prependStringifier(new QuotedStringifier($quoter)); + $stringifier->prependStringifier(new ListedStringifier($stringifier)); return $stringifier; } diff --git a/library/Message/Stringifier/ListedStringifier.php b/library/Message/Stringifier/ListedStringifier.php new file mode 100644 index 000000000..b6cded8c3 --- /dev/null +++ b/library/Message/Stringifier/ListedStringifier.php @@ -0,0 +1,46 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Respect\Validation\Message\Stringifier; + +use Respect\Stringifier\Stringifier; +use Respect\Validation\Message\Placeholder\Listed; + +use function array_map; +use function array_pop; +use function count; +use function implode; +use function sprintf; + +final class ListedStringifier implements Stringifier +{ + public function __construct( + private readonly Stringifier $stringifier + ) { + } + + public function stringify(mixed $raw, int $depth): ?string + { + if (!$raw instanceof Listed) { + return null; + } + + if (count($raw->values) === 0) { + return null; + } + + $strings = array_map(fn ($value) => $this->stringifier->stringify($value, $depth + 1), $raw->values); + if (count($strings) < 3) { + return implode(sprintf(' %s ', $raw->lastGlue), $strings); + } + $lastString = array_pop($strings); + + return sprintf('%s, %s %s', implode(', ', $strings), $raw->lastGlue, $lastString); + } +} diff --git a/tests/feature/TranslatorTest.php b/tests/feature/TranslatorTest.php index 45f5547e1..c0a4f550c 100644 --- a/tests/feature/TranslatorTest.php +++ b/tests/feature/TranslatorTest.php @@ -11,7 +11,7 @@ use Respect\Validation\Validator; use Respect\Validation\ValidatorDefaults; -test('Scenario #1', expectFullMessage( +test('Various translations', expectFullMessage( function (): void { ValidatorDefaults::setTranslator(new ArrayTranslator([ 'All the required rules must pass for {{name}}' => 'Todas as regras requeridas devem passar para {{name}}', @@ -32,7 +32,7 @@ function (): void { FULL_MESSAGE, )); -test('Scenario #2', expectMessage( +test('DateTimeDiff', expectMessage( function (): void { ValidatorDefaults::setTranslator(new ArrayTranslator([ 'years' => 'anos', @@ -44,3 +44,27 @@ function (): void { }, 'O número de anos entre agora e "1972-02-09" deve ser igual a 2', )); + +test('Using "listOr"', expectMessage( + function (): void { + ValidatorDefaults::setTranslator(new ArrayTranslator([ + 'Your name must be {{haystack|listOr}}' => 'Seu nome deve ser {{haystack|listOr}}', + 'or' => 'ou', + ])); + + v::templated(v::in(['Respect', 'Validation']), 'Your name must be {{haystack|listOr}}')->assert(''); + }, + 'Seu nome deve ser "Respect" ou "Validation"', +)); + +test('Using "listAnd"', expectMessage( + function (): void { + ValidatorDefaults::setTranslator(new ArrayTranslator([ + '{{haystack|listAnd}} are the only possible names' => '{{haystack|listAnd}} são os únicos nomes possíveis', + 'and' => 'e', + ])); + + v::templated(v::in(['Respect', 'Validation']), '{{haystack|listAnd}} are the only possible names')->assert(''); + }, + '"Respect" e "Validation" são os únicos nomes possíveis', +)); diff --git a/tests/unit/Message/Stringifier/ListedStringifierTest.php b/tests/unit/Message/Stringifier/ListedStringifierTest.php new file mode 100644 index 000000000..8a0382b88 --- /dev/null +++ b/tests/unit/Message/Stringifier/ListedStringifierTest.php @@ -0,0 +1,60 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +namespace Respect\Validation\Message\Stringifier; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use Respect\Stringifier\Stringifiers\JsonEncodableStringifier; +use Respect\Validation\Message\Placeholder\Listed; +use Respect\Validation\Test\TestCase; + +#[CoversClass(ListedStringifier::class)] +final class ListedStringifierTest extends TestCase +{ + #[Test] + #[DataProvider('providerForAnyValues')] + public function itShouldNotStringifyWhenValueIsNotAnInstanceOfListed(mixed $value): void + { + $quoter = new JsonEncodableStringifier(); + $stringifier = new ListedStringifier($quoter); + + self::assertNull($stringifier->stringify($value, 0)); + } + + #[Test] + public function itShouldNotStringifyEmptyListed(): void + { + $stringifier = new ListedStringifier(new JsonEncodableStringifier()); + + self::assertNull($stringifier->stringify(new Listed([], '-'), 0)); + } + + #[Test] + #[DataProvider('providerForListed')] + public function itShouldStringifyWhenValueIsAnInstanceOfListed(Listed $listed, string $expected): void + { + $stringifier = new ListedStringifier(new JsonEncodableStringifier()); + + $actual = $stringifier->stringify($listed, 0); + + self::assertSame($expected, $actual); + } + + /** @return array */ + public static function providerForListed(): array + { + return [ + '1 item' => [new Listed([1], 'and'), '1'], + '2 items' => [new Listed([1, 2], 'and'), '1 and 2'], + '3 items' => [new Listed([1, 2, 3], 'or'), '1, 2, or 3'], + ]; + } +}