diff --git a/docs/rules/Max.md b/docs/rules/Max.md index ee4e7db33..4bc481271 100644 --- a/docs/rules/Max.md +++ b/docs/rules/Max.md @@ -24,12 +24,6 @@ empty, the validation will fail. ### `Max::TEMPLATE_STANDARD` -| Mode | Template | -|------------|-----------------------------| -| `default` | As the maximum of {{name}}, | -| `inverted` | As the maximum of {{name}}, | - -### `Max::TEMPLATE_NAMED` | Mode | Template | |------------|----------------| diff --git a/docs/rules/Min.md b/docs/rules/Min.md index c8fff3c05..14df2fb64 100644 --- a/docs/rules/Min.md +++ b/docs/rules/Min.md @@ -24,17 +24,10 @@ empty, the validation will fail. ### `Min::TEMPLATE_STANDARD` -| Mode | Template | -|------------|-------------------------------| -| `default` | As the minimum from {{name}}, | -| `inverted` | As the minimum from {{name}}, | - -### `Min::TEMPLATE_NAMED` - | Mode | Template | |------------|------------------| -| `default` | The minimum from | -| `inverted` | The minimum from | +| `default` | The minimum of | +| `inverted` | The minimum of | ## Template placeholders diff --git a/library/Rules/Core/ArrayAggregateFunction.php b/library/Rules/Core/ArrayAggregateFunction.php new file mode 100644 index 000000000..6747390ae --- /dev/null +++ b/library/Rules/Core/ArrayAggregateFunction.php @@ -0,0 +1,49 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +namespace Respect\Validation\Rules\Core; + +use Respect\Validation\Result; + +use function array_map; + +abstract class ArrayAggregateFunction extends FilteredNonEmptyArray +{ + protected string $idPrefix; + + /** + * This function should extract the aggregate data from the input array + * + * @param non-empty-array $input + */ + abstract protected function extractAggregate(array $input): mixed; + + /** @param non-empty-array $input */ + protected function evaluateNonEmptyArray(array $input): Result + { + $aggregate = $this->extractAggregate($input); + + return $this->enrichResult($input, $this->rule->evaluate($aggregate)); + } + + private function enrichResult(mixed $input, Result $result): Result + { + if (!$result->allowsSubsequent()) { + return $result + ->withInput($input) + ->withChildren( + ...array_map(fn(Result $child) => $this->enrichResult($input, $child), $result->children) + ); + } + + return (new Result($result->isValid, $input, $this, id: $result->id)) + ->withPrefixedId($this->idPrefix) + ->withSubsequent($result->withInput($input)); + } +} diff --git a/library/Rules/Max.php b/library/Rules/Max.php index 3f2cf62bc..2bf888c02 100644 --- a/library/Rules/Max.php +++ b/library/Rules/Max.php @@ -11,24 +11,19 @@ use Attribute; use Respect\Validation\Message\Template; -use Respect\Validation\Result; -use Respect\Validation\Rules\Core\FilteredNonEmptyArray; +use Respect\Validation\Rules\Core\ArrayAggregateFunction; use function max; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -#[Template('As the maximum of {{name}},', 'As the maximum of {{name}},')] -#[Template('The maximum of', 'The maximum of', self::TEMPLATE_NAMED)] -final class Max extends FilteredNonEmptyArray +#[Template('The maximum of', 'The maximum of')] +final class Max extends ArrayAggregateFunction { - public const TEMPLATE_NAMED = '__named__'; + protected string $idPrefix = 'max'; /** @param non-empty-array $input */ - protected function evaluateNonEmptyArray(array $input): Result + protected function extractAggregate(array $input): mixed { - $result = $this->rule->evaluate(max($input))->withPrefixedId('max'); - $template = $this->getName() === null ? self::TEMPLATE_STANDARD : self::TEMPLATE_NAMED; - - return (new Result($result->isValid, $input, $this, [], $template, id: $result->id))->withSubsequent($result); + return max($input); } } diff --git a/library/Rules/Min.php b/library/Rules/Min.php index 2180da2c6..47d06ea31 100644 --- a/library/Rules/Min.php +++ b/library/Rules/Min.php @@ -11,24 +11,19 @@ use Attribute; use Respect\Validation\Message\Template; -use Respect\Validation\Result; -use Respect\Validation\Rules\Core\FilteredNonEmptyArray; +use Respect\Validation\Rules\Core\ArrayAggregateFunction; use function min; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -#[Template('As the minimum from {{name}},', 'As the minimum from {{name}},')] -#[Template('The minimum from', 'The minimum from', self::TEMPLATE_NAMED)] -final class Min extends FilteredNonEmptyArray +#[Template('The minimum of', 'The minimum of')] +final class Min extends ArrayAggregateFunction { - public const TEMPLATE_NAMED = '__named__'; + protected string $idPrefix = 'min'; /** @param non-empty-array $input */ - protected function evaluateNonEmptyArray(array $input): Result + protected function extractAggregate(array $input): mixed { - $result = $this->rule->evaluate(min($input))->withPrefixedId('min'); - $template = $this->getName() === null ? self::TEMPLATE_STANDARD : self::TEMPLATE_NAMED; - - return (new Result($result->isValid, $input, $this, [], $template, id: $result->id))->withSubsequent($result); + return min($input); } } diff --git a/tests/feature/Rules/EachTest.php b/tests/feature/Rules/EachTest.php index b2f19a2a0..7e1d29d31 100644 --- a/tests/feature/Rules/EachTest.php +++ b/tests/feature/Rules/EachTest.php @@ -239,3 +239,55 @@ 'intType.3' => 'Wrapped must be an integer', ] )); + +test('Chained wrapped rule', expectAll( + fn() => v::each(v::between(5, 7)->odd())->assert([2, 4]), + '2 must be between 5 and 7', + <<<'FULL_MESSAGE' + - Each item in `[2, 4]` must be valid + - All of the required rules must pass for 2 + - 2 must be between 5 and 7 + - 2 must be an odd number + - All of the required rules must pass for 4 + - 4 must be between 5 and 7 + - 4 must be an odd number + FULL_MESSAGE, + [ + '__root__' => 'Each item in `[2, 4]` must be valid', + 'allOf.1' => [ + '__root__' => 'All of the required rules must pass for 2', + 'between' => '2 must be between 5 and 7', + 'odd' => '2 must be an odd number', + ], + 'allOf.2' => [ + '__root__' => 'All of the required rules must pass for 4', + 'between' => '4 must be between 5 and 7', + 'odd' => '4 must be an odd number', + ], + ] +)); + +test('Multiple nested rules', expectAll( + fn() => v::each(v::arrayType()->key('my_int', v::intType()->odd()))->assert([['not_int' => 'wrong'], ['my_int' => 2], 'not an array']), + 'my_int must be present', + <<<'FULL_MESSAGE' + - Each item in `[["not_int": "wrong"], ["my_int": 2], "not an array"]` must be valid + - These rules must pass for `["not_int": "wrong"]` + - my_int must be present + - These rules must pass for `["my_int": 2]` + - my_int must be an odd number + - All of the required rules must pass for "not an array" + - "not an array" must be an array + - my_int must be present + FULL_MESSAGE, + [ + '__root__' => 'Each item in `[["not_int": "wrong"], ["my_int": 2], "not an array"]` must be valid', + 'allOf.1' => 'my_int must be present', + 'allOf.2' => 'my_int must be an odd number', + 'allOf.3' => [ + '__root__' => 'All of the required rules must pass for "not an array"', + 'arrayType' => '"not an array" must be an array', + 'my_int' => 'my_int must be present', + ], + ] +)); diff --git a/tests/feature/Rules/MaxTest.php b/tests/feature/Rules/MaxTest.php index 89dd95f69..9bc2853ad 100644 --- a/tests/feature/Rules/MaxTest.php +++ b/tests/feature/Rules/MaxTest.php @@ -23,16 +23,16 @@ test('Default', expectAll( fn() => v::max(v::negative())->assert([1, 2, 3]), - 'As the maximum of `[1, 2, 3]`, 3 must be a negative number', - '- As the maximum of `[1, 2, 3]`, 3 must be a negative number', - ['maxNegative' => 'As the maximum of `[1, 2, 3]`, 3 must be a negative number'] + 'The maximum of `[1, 2, 3]` must be a negative number', + '- The maximum of `[1, 2, 3]` must be a negative number', + ['maxNegative' => 'The maximum of `[1, 2, 3]` must be a negative number'] )); test('Inverted', expectAll( fn() => v::not(v::max(v::negative()))->assert([-3, -2, -1]), - 'As the maximum of `[-3, -2, -1]`, -1 must not be a negative number', - '- As the maximum of `[-3, -2, -1]`, -1 must not be a negative number', - ['notMaxNegative' => 'As the maximum of `[-3, -2, -1]`, -1 must not be a negative number'] + 'The maximum of `[-3, -2, -1]` must not be a negative number', + '- The maximum of `[-3, -2, -1]` must not be a negative number', + ['notMaxNegative' => 'The maximum of `[-3, -2, -1]` must not be a negative number'] )); test('With wrapped name, default', expectAll( @@ -69,3 +69,18 @@ '- The maximum of the value is not what we expect', ['maxNegative' => 'The maximum of the value is not what we expect'] )); + +test('Chained wrapped rule', expectAll( + fn() => v::max(v::between(5, 7)->odd())->assert([1, 2, 3, 4]), + 'The maximum of `[1, 2, 3, 4]` must be between 5 and 7', + <<<'FULL_MESSAGE' + - All of the required rules must pass for `[1, 2, 3, 4]` + - The maximum of `[1, 2, 3, 4]` must be between 5 and 7 + - The maximum of `[1, 2, 3, 4]` must be an odd number + FULL_MESSAGE, + [ + '__root__' => 'All of the required rules must pass for `[1, 2, 3, 4]`', + 'maxBetween' => 'The maximum of `[1, 2, 3, 4]` must be between 5 and 7', + 'maxOdd' => 'The maximum of `[1, 2, 3, 4]` must be an odd number', + ] +)); diff --git a/tests/feature/Rules/MinTest.php b/tests/feature/Rules/MinTest.php index 17be7d042..1fb0557b9 100644 --- a/tests/feature/Rules/MinTest.php +++ b/tests/feature/Rules/MinTest.php @@ -9,16 +9,16 @@ test('Default', expectAll( fn() => v::min(v::equals(1))->assert([2, 3]), - 'As the minimum from `[2, 3]`, 2 must be equal to 1', - '- As the minimum from `[2, 3]`, 2 must be equal to 1', - ['minEquals' => 'As the minimum from `[2, 3]`, 2 must be equal to 1'] + 'The minimum of `[2, 3]` must be equal to 1', + '- The minimum of `[2, 3]` must be equal to 1', + ['minEquals' => 'The minimum of `[2, 3]` must be equal to 1'] )); test('Inverted', expectAll( fn() => v::not(v::min(v::equals(1)))->assert([1, 2, 3]), - 'As the minimum from `[1, 2, 3]`, 1 must not be equal to 1', - '- As the minimum from `[1, 2, 3]`, 1 must not be equal to 1', - ['notMinEquals' => 'As the minimum from `[1, 2, 3]`, 1 must not be equal to 1'] + 'The minimum of `[1, 2, 3]` must not be equal to 1', + '- The minimum of `[1, 2, 3]` must not be equal to 1', + ['notMinEquals' => 'The minimum of `[1, 2, 3]` must not be equal to 1'] )); test('With template', expectAll( @@ -30,7 +30,22 @@ test('With name', expectAll( fn() => v::min(v::equals(1))->setName('Options')->assert([2, 3]), - 'The minimum from Options must be equal to 1', - '- The minimum from Options must be equal to 1', - ['minEquals' => 'The minimum from Options must be equal to 1'] + 'The minimum of Options must be equal to 1', + '- The minimum of Options must be equal to 1', + ['minEquals' => 'The minimum of Options must be equal to 1'] +)); + +test('Chained wrapped rule', expectAll( + fn() => v::min(v::between(5, 7)->odd())->assert([2, 3, 4]), + 'The minimum of `[2, 3, 4]` must be between 5 and 7', + <<<'FULL_MESSAGE' + - All of the required rules must pass for `[2, 3, 4]` + - The minimum of `[2, 3, 4]` must be between 5 and 7 + - The minimum of `[2, 3, 4]` must be an odd number + FULL_MESSAGE, + [ + '__root__' => 'All of the required rules must pass for `[2, 3, 4]`', + 'minBetween' => 'The minimum of `[2, 3, 4]` must be between 5 and 7', + 'minOdd' => 'The minimum of `[2, 3, 4]` must be an odd number', + ] )); diff --git a/tests/feature/Transformers/PrefixTest.php b/tests/feature/Transformers/PrefixTest.php index 14c583d35..6dbd205d7 100644 --- a/tests/feature/Transformers/PrefixTest.php +++ b/tests/feature/Transformers/PrefixTest.php @@ -25,16 +25,16 @@ test('Max', expectAll( fn() => v::maxOdd()->assert([1, 2, 3, 4]), - 'As the maximum of `[1, 2, 3, 4]`, 4 must be an odd number', - '- As the maximum of `[1, 2, 3, 4]`, 4 must be an odd number', - ['maxOdd' => 'As the maximum of `[1, 2, 3, 4]`, 4 must be an odd number'] + 'The maximum of `[1, 2, 3, 4]` must be an odd number', + '- The maximum of `[1, 2, 3, 4]` must be an odd number', + ['maxOdd' => 'The maximum of `[1, 2, 3, 4]` must be an odd number'] )); test('Min', expectAll( fn() => v::minEven()->assert([1, 2, 3]), - 'As the minimum from `[1, 2, 3]`, 1 must be an even number', - '- As the minimum from `[1, 2, 3]`, 1 must be an even number', - ['minEven' => 'As the minimum from `[1, 2, 3]`, 1 must be an even number'] + 'The minimum of `[1, 2, 3]` must be an even number', + '- The minimum of `[1, 2, 3]` must be an even number', + ['minEven' => 'The minimum of `[1, 2, 3]` must be an even number'] )); test('Not', expectAll(