From cea77d2a463504cbd1309326fa3ce64c3ddc456a Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Thu, 29 Feb 2024 22:31:15 +0100 Subject: [PATCH] Recreate "Max" rule The "Max" rule is not a transformation, validating the maximum value in the input against a given rule. Signed-off-by: Henrique Moody --- docs/08-list-of-rules-by-category.md | 3 + docs/rules/Between.md | 1 + docs/rules/GreaterThan.md | 1 + docs/rules/GreaterThanOrEqual.md | 1 + docs/rules/IterableType.md | 1 + docs/rules/LessThan.md | 1 + docs/rules/LessThanOrEqual.md | 1 + docs/rules/Max.md | 47 +++++++++++++ docs/rules/Min.md | 1 + docs/rules/NotEmpty.md | 1 + library/ChainedValidator.php | 2 + library/Rules/Max.php | 32 +++++++++ library/StaticValidator.php | 2 + tests/integration/rules/max.phpt | 100 +++++++++++++++++++++++++++ tests/unit/Rules/MaxTest.php | 85 +++++++++++++++++++++++ 15 files changed, 279 insertions(+) create mode 100644 docs/rules/Max.md create mode 100644 library/Rules/Max.php create mode 100644 tests/integration/rules/max.phpt create mode 100644 tests/unit/Rules/MaxTest.php diff --git a/docs/08-list-of-rules-by-category.md b/docs/08-list-of-rules-by-category.md index 906a16a9..70243424 100644 --- a/docs/08-list-of-rules-by-category.md +++ b/docs/08-list-of-rules-by-category.md @@ -51,6 +51,7 @@ - [In](rules/In.md) - [LessThan](rules/LessThan.md) - [LessThanOrEqual](rules/LessThanOrEqual.md) +- [Max](rules/Max.md) - [Min](rules/Min.md) ## Composite @@ -249,6 +250,7 @@ - [Call](rules/Call.md) - [Each](rules/Each.md) +- [Max](rules/Max.md) - [Min](rules/Min.md) ## Types @@ -359,6 +361,7 @@ - [Lowercase](rules/Lowercase.md) - [Luhn](rules/Luhn.md) - [MacAddress](rules/MacAddress.md) +- [Max](rules/Max.md) - [MaxAge](rules/MaxAge.md) - [Mimetype](rules/Mimetype.md) - [Min](rules/Min.md) diff --git a/docs/rules/Between.md b/docs/rules/Between.md index 210f712c..a7749ea6 100644 --- a/docs/rules/Between.md +++ b/docs/rules/Between.md @@ -36,4 +36,5 @@ See also: - [Length](Length.md) - [LessThan](LessThan.md) - [LessThanOrEqual](LessThanOrEqual.md) +- [Max](Max.md) - [Min](Min.md) diff --git a/docs/rules/GreaterThan.md b/docs/rules/GreaterThan.md index 66ce5b06..fbdf7682 100644 --- a/docs/rules/GreaterThan.md +++ b/docs/rules/GreaterThan.md @@ -30,4 +30,5 @@ See also: - [Between](Between.md) - [GreaterThanOrEqual](GreaterThanOrEqual.md) - [LessThanOrEqual](LessThanOrEqual.md) +- [Max](Max.md) - [Min](Min.md) diff --git a/docs/rules/GreaterThanOrEqual.md b/docs/rules/GreaterThanOrEqual.md index 62669be1..e4923c7a 100644 --- a/docs/rules/GreaterThanOrEqual.md +++ b/docs/rules/GreaterThanOrEqual.md @@ -36,6 +36,7 @@ See also: - [Length](Length.md) - [LessThan](LessThan.md) - [LessThanOrEqual](LessThanOrEqual.md) +- [Max](Max.md) - [MaxAge](MaxAge.md) - [Min](Min.md) - [MinAge](MinAge.md) diff --git a/docs/rules/IterableType.md b/docs/rules/IterableType.md index 293522da..d648862e 100644 --- a/docs/rules/IterableType.md +++ b/docs/rules/IterableType.md @@ -33,3 +33,4 @@ See also: - [Each](Each.md) - [Instance](Instance.md) - [IterableVal](IterableVal.md) +- [Max](Max.md) diff --git a/docs/rules/LessThan.md b/docs/rules/LessThan.md index a5e6dede..1a65c010 100644 --- a/docs/rules/LessThan.md +++ b/docs/rules/LessThan.md @@ -30,4 +30,5 @@ See also: - [Between](Between.md) - [GreaterThanOrEqual](GreaterThanOrEqual.md) - [LessThanOrEqual](LessThanOrEqual.md) +- [Max](Max.md) - [Min](Min.md) diff --git a/docs/rules/LessThanOrEqual.md b/docs/rules/LessThanOrEqual.md index 6992a7d9..5844a200 100644 --- a/docs/rules/LessThanOrEqual.md +++ b/docs/rules/LessThanOrEqual.md @@ -35,6 +35,7 @@ See also: - [GreaterThan](GreaterThan.md) - [GreaterThanOrEqual](GreaterThanOrEqual.md) - [LessThan](LessThan.md) +- [Max](Max.md) - [MaxAge](MaxAge.md) - [Min](Min.md) - [MinAge](MinAge.md) diff --git a/docs/rules/Max.md b/docs/rules/Max.md new file mode 100644 index 00000000..386229fd --- /dev/null +++ b/docs/rules/Max.md @@ -0,0 +1,47 @@ +# Max + +- `Max(Validatable $rule)` + +Validates the maximum value of the input against a given rule. + +```php +v::max(v::equals(30))->validate([10, 20, 30]); // true + +v::max(v::between('e', 'g'))->validate(['b', 'd', 'f']); // true + +v::max(v::greaterThan(new DateTime('today'))) + ->validate([new DateTime('yesterday'), new DateTime('tomorrow')]); // true + +v::max(v::greaterThan(15))->validate([4, 8, 12]); // false +``` + +## Note + +This rule uses [IterableType](IterableType.md) and [NotEmpty](NotEmpty.md) internally. If an input is non-iterable or +empty, the validation will fail. + +## Categorization + +- Comparisons +- Transformations + +## Changelog + +| Version | Description | +|--------:|-----------------------------| +| 3.0.0 | Became a transformation | +| 2.0.0 | Became always inclusive | +| 1.0.0 | Became inclusive by default | +| 0.3.9 | Created | + +*** +See also: + +- [Between](Between.md) +- [GreaterThan](GreaterThan.md) +- [GreaterThanOrEqual](GreaterThanOrEqual.md) +- [IterableType](IterableType.md) +- [LessThan](LessThan.md) +- [LessThanOrEqual](LessThanOrEqual.md) +- [Min](Min.md) +- [NotEmpty](NotEmpty.md) diff --git a/docs/rules/Min.md b/docs/rules/Min.md index 906f6283..25329274 100644 --- a/docs/rules/Min.md +++ b/docs/rules/Min.md @@ -43,4 +43,5 @@ See also: - [GreaterThanOrEqual](GreaterThanOrEqual.md) - [LessThan](LessThan.md) - [LessThanOrEqual](LessThanOrEqual.md) +- [Max](Max.md) - [NotEmpty](NotEmpty.md) diff --git a/docs/rules/NotEmpty.md b/docs/rules/NotEmpty.md index a6c2a7c5..a7149d60 100644 --- a/docs/rules/NotEmpty.md +++ b/docs/rules/NotEmpty.md @@ -49,6 +49,7 @@ Version | Description See also: - [Each](Each.md) +- [Max](Max.md) - [Min](Min.md) - [NoWhitespace](NoWhitespace.md) - [NotBlank](NotBlank.md) diff --git a/library/ChainedValidator.php b/library/ChainedValidator.php index 1261ccb5..ab94717a 100644 --- a/library/ChainedValidator.php +++ b/library/ChainedValidator.php @@ -198,6 +198,8 @@ interface ChainedValidator extends Validatable public function lessThanOrEqual(mixed $compareTo): ChainedValidator; + public function max(Validatable $rule): ChainedValidator; + public function maxAge(int $age, ?string $format = null): ChainedValidator; public function min(Validatable $rule): ChainedValidator; diff --git a/library/Rules/Max.php b/library/Rules/Max.php new file mode 100644 index 00000000..c2abd5f4 --- /dev/null +++ b/library/Rules/Max.php @@ -0,0 +1,32 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +namespace Respect\Validation\Rules; + +use Respect\Validation\Message\Template; +use Respect\Validation\Result; +use Respect\Validation\Rules\Core\FilteredNonEmptyArray; + +use function max; + +#[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 +{ + public const TEMPLATE_NAMED = '__named__'; + + /** @param non-empty-array $input */ + protected function evaluateNonEmptyArray(array $input): Result + { + $result = $this->rule->evaluate(max($input)); + $template = $this->getName() === null ? self::TEMPLATE_STANDARD : self::TEMPLATE_NAMED; + + return (new Result($result->isValid, $input, $this, [], $template))->withNextSibling($result); + } +} diff --git a/library/StaticValidator.php b/library/StaticValidator.php index 8a8fb2b2..e2e21ec0 100644 --- a/library/StaticValidator.php +++ b/library/StaticValidator.php @@ -200,6 +200,8 @@ interface StaticValidator public static function lessThanOrEqual(mixed $compareTo): ChainedValidator; + public static function max(Validatable $rule): ChainedValidator; + public static function maxAge(int $age, ?string $format = null): ChainedValidator; public static function min(Validatable $rule): ChainedValidator; diff --git a/tests/integration/rules/max.phpt b/tests/integration/rules/max.phpt new file mode 100644 index 00000000..61852ceb --- /dev/null +++ b/tests/integration/rules/max.phpt @@ -0,0 +1,100 @@ +--FILE-- + [v::max(v::negative()), $nonIterable], + 'Empty' => [v::max(v::negative()), $empty], + 'Default' => [v::max(v::negative()), $default], + 'Negative' => [v::not(v::max(v::negative())), $negative], + 'With wrapped name, default' => [v::max(v::negative()->setName('Wrapped'))->setName('Wrapper'), $default], + 'With wrapper name, default' => [v::max(v::negative())->setName('Wrapper'), $default], + 'With wrapped name, negative' => [v::not(v::max(v::negative()->setName('Wrapped')))->setName('Wrapper'), $negative], + 'With wrapper name, negative' => [v::not(v::max(v::negative()))->setName('Wrapper'), $negative], + 'With template, default' => [v::max(v::negative()), $default, 'The maximum of the value is not what we expect'], +]); +?> +--EXPECT-- +Non-iterable +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +`null` must be of type iterable +- `null` must be of type iterable +[ + 'max' => '`null` must be of type iterable', +] + +Empty +⎺⎺⎺⎺⎺ +The value must not be empty +- The value must not be empty +[ + 'max' => 'The value must not be empty', +] + +Default +⎺⎺⎺⎺⎺⎺⎺ +As the maximum of `[1, 2, 3]`, 3 must be negative +- As the maximum of `[1, 2, 3]`, 3 must be negative +[ + 'max' => 'As the maximum of `[1, 2, 3]`, 3 must be negative', +] + +Negative +⎺⎺⎺⎺⎺⎺⎺⎺ +As the maximum of `[-3, -2, -1]`, -1 must not be negative +- As the maximum of `[-3, -2, -1]`, -1 must not be negative +[ + 'max' => 'As the maximum of `[-3, -2, -1]`, -1 must not be negative', +] + +With wrapped name, default +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The maximum of Wrapped must be negative +- The maximum of Wrapped must be negative +[ + 'Wrapped' => 'The maximum of Wrapped must be negative', +] + +With wrapper name, default +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The maximum of Wrapper must be negative +- The maximum of Wrapper must be negative +[ + 'Wrapper' => 'The maximum of Wrapper must be negative', +] + +With wrapped name, negative +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The maximum of Wrapped must not be negative +- The maximum of Wrapped must not be negative +[ + 'Wrapped' => 'The maximum of Wrapped must not be negative', +] + +With wrapper name, negative +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The maximum of Wrapper must not be negative +- The maximum of Wrapper must not be negative +[ + 'Wrapper' => 'The maximum of Wrapper must not be negative', +] + +With template, default +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The maximum of the value is not what we expect +- The maximum of the value is not what we expect +[ + 'max' => 'The maximum of the value is not what we expect', +] diff --git a/tests/unit/Rules/MaxTest.php b/tests/unit/Rules/MaxTest.php new file mode 100644 index 00000000..fe83bedf --- /dev/null +++ b/tests/unit/Rules/MaxTest.php @@ -0,0 +1,85 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +namespace Respect\Validation\Rules; + +use DateTimeImmutable; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; +use Respect\Validation\Test\Rules\Stub; +use Respect\Validation\Test\TestCase; + +#[Group('rule')] +#[CoversClass(Max::class)] +final class MaxTest extends TestCase +{ + #[Test] + #[DataProvider('providerForNonIterableValues')] + public function itShouldInvalidateNonIterableValues(mixed $input): void + { + $rule = new Max(Stub::daze()); + + self::assertInvalidInput($rule, $input); + } + + /** @param iterable $input */ + #[Test] + #[DataProvider('providerForEmptyIterableValues')] + public function itShouldInvalidateEmptyIterableValues(iterable $input): void + { + $rule = new Max(Stub::daze()); + + self::assertInvalidInput($rule, $input); + } + + /** @param iterable $input */ + #[Test] + #[DataProvider('providerForNonEmptyIterableValues')] + public function itShouldValidateNonEmptyIterableValuesWhenWrappedRulePasses(iterable $input): void + { + $rule = new Max(Stub::pass(1)); + + self::assertValidInput($rule, $input); + } + + /** @param iterable $input */ + #[Test] + #[DataProvider('providerForMaxValues')] + public function itShouldValidateWithTheMaximumValue(iterable $input, mixed $min): void + { + $wrapped = Stub::pass(1); + + $rule = new Max($wrapped); + $rule->evaluate($input); + + self::assertSame($min, $wrapped->inputs[0]); + } + + /** @return array, mixed}> */ + public static function providerForMaxValues(): array + { + $yesterday = new DateTimeImmutable('yesterday'); + $today = new DateTimeImmutable('today'); + $tomorrow = new DateTimeImmutable('tomorrow'); + + return [ + '3 DateTime objects' => [[$yesterday, $today, $tomorrow], $tomorrow], + '2 DateTime objects' => [[$yesterday, $today], $today], + '1 DateTime objects' => [[$yesterday], $yesterday], + '3 integers' => [[1, 2, 3], 3], + '2 integers' => [[1, 2], 2], + '1 integer' => [[1], 1], + '3 characters' => [['a', 'b', 'c'], 'c'], + '2 characters' => [['a', 'b'], 'b'], + '1 character' => [['a'], 'a'], + ]; + } +}