Skip to content

Commit

Permalink
Recreate "Max" rule
Browse files Browse the repository at this point in the history
The "Max" rule is not a transformation, validating the maximum value in
the input against a given rule.

Signed-off-by: Henrique Moody <[email protected]>
  • Loading branch information
henriquemoody committed Mar 3, 2024
1 parent 81befe8 commit cea77d2
Show file tree
Hide file tree
Showing 15 changed files with 279 additions and 0 deletions.
3 changes: 3 additions & 0 deletions docs/08-list-of-rules-by-category.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -249,6 +250,7 @@

- [Call](rules/Call.md)
- [Each](rules/Each.md)
- [Max](rules/Max.md)
- [Min](rules/Min.md)

## Types
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/rules/Between.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ See also:
- [Length](Length.md)
- [LessThan](LessThan.md)
- [LessThanOrEqual](LessThanOrEqual.md)
- [Max](Max.md)
- [Min](Min.md)
1 change: 1 addition & 0 deletions docs/rules/GreaterThan.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ See also:
- [Between](Between.md)
- [GreaterThanOrEqual](GreaterThanOrEqual.md)
- [LessThanOrEqual](LessThanOrEqual.md)
- [Max](Max.md)
- [Min](Min.md)
1 change: 1 addition & 0 deletions docs/rules/GreaterThanOrEqual.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 1 addition & 0 deletions docs/rules/IterableType.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ See also:
- [Each](Each.md)
- [Instance](Instance.md)
- [IterableVal](IterableVal.md)
- [Max](Max.md)
1 change: 1 addition & 0 deletions docs/rules/LessThan.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ See also:
- [Between](Between.md)
- [GreaterThanOrEqual](GreaterThanOrEqual.md)
- [LessThanOrEqual](LessThanOrEqual.md)
- [Max](Max.md)
- [Min](Min.md)
1 change: 1 addition & 0 deletions docs/rules/LessThanOrEqual.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
47 changes: 47 additions & 0 deletions docs/rules/Max.md
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions docs/rules/Min.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ See also:
- [GreaterThanOrEqual](GreaterThanOrEqual.md)
- [LessThan](LessThan.md)
- [LessThanOrEqual](LessThanOrEqual.md)
- [Max](Max.md)
- [NotEmpty](NotEmpty.md)
1 change: 1 addition & 0 deletions docs/rules/NotEmpty.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Version | Description
See also:

- [Each](Each.md)
- [Max](Max.md)
- [Min](Min.md)
- [NoWhitespace](NoWhitespace.md)
- [NotBlank](NotBlank.md)
Expand Down
2 changes: 2 additions & 0 deletions library/ChainedValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ public function macAddress(): ChainedValidator;

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;
Expand Down
32 changes: 32 additions & 0 deletions library/Rules/Max.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/*
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
* 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<mixed> $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);
}
}
2 changes: 2 additions & 0 deletions library/StaticValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ public static function macAddress(): ChainedValidator;

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;
Expand Down
100 changes: 100 additions & 0 deletions tests/integration/rules/max.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
--FILE--
<?php

declare(strict_types=1);

require 'vendor/autoload.php';

use Respect\Validation\Validator as v;

$empty = [];
$nonIterable = null;
$default = [1, 2, 3];
$negative = [-3, -2, -1];


run([
// Simple
'Non-iterable' => [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',
]
85 changes: 85 additions & 0 deletions tests/unit/Rules/MaxTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

/*
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
* 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<mixed> $input */
#[Test]
#[DataProvider('providerForEmptyIterableValues')]
public function itShouldInvalidateEmptyIterableValues(iterable $input): void
{
$rule = new Max(Stub::daze());

self::assertInvalidInput($rule, $input);
}

/** @param iterable<mixed> $input */
#[Test]
#[DataProvider('providerForNonEmptyIterableValues')]
public function itShouldValidateNonEmptyIterableValuesWhenWrappedRulePasses(iterable $input): void
{
$rule = new Max(Stub::pass(1));

self::assertValidInput($rule, $input);
}

/** @param iterable<mixed> $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<string, array{iterable<mixed>, 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'],
];
}
}

0 comments on commit cea77d2

Please sign in to comment.