Skip to content

Commit

Permalink
Create "Min" rule
Browse files Browse the repository at this point in the history
With this rule, we introduce a new type of rule, which is only possible
due to the changes in the validation engine.

Signed-off-by: Henrique Moody <[email protected]>
  • Loading branch information
henriquemoody committed Feb 27, 2024
1 parent 2f12b6c commit cc96ee9
Show file tree
Hide file tree
Showing 15 changed files with 352 additions and 52 deletions.
6 changes: 6 additions & 0 deletions docs/08-list-of-rules-by-category.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# List of rules by category

## Aggregations

- [Min](rules/Min.md)

## Arrays

- [ArrayType](rules/ArrayType.md)
Expand Down Expand Up @@ -51,6 +55,7 @@
- [In](rules/In.md)
- [LessThan](rules/LessThan.md)
- [LessThanOrEqual](rules/LessThanOrEqual.md)
- [Min](rules/Min.md)

## Composite

Expand Down Expand Up @@ -352,6 +357,7 @@
- [MacAddress](rules/MacAddress.md)
- [MaxAge](rules/MaxAge.md)
- [Mimetype](rules/Mimetype.md)
- [Min](rules/Min.md)
- [MinAge](rules/MinAge.md)
- [Multiple](rules/Multiple.md)
- [Negative](rules/Negative.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,3 +36,4 @@ See also:
- [Length](Length.md)
- [LessThan](LessThan.md)
- [LessThanOrEqual](LessThanOrEqual.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,3 +30,4 @@ See also:
- [Between](Between.md)
- [GreaterThanOrEqual](GreaterThanOrEqual.md)
- [LessThanOrEqual](LessThanOrEqual.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 @@ -37,4 +37,5 @@ See also:
- [LessThan](LessThan.md)
- [LessThanOrEqual](LessThanOrEqual.md)
- [MaxAge](MaxAge.md)
- [Min](Min.md)
- [MinAge](MinAge.md)
1 change: 1 addition & 0 deletions docs/rules/LessThan.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ See also:
- [Between](Between.md)
- [GreaterThanOrEqual](GreaterThanOrEqual.md)
- [LessThanOrEqual](LessThanOrEqual.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 @@ -36,4 +36,5 @@ See also:
- [GreaterThanOrEqual](GreaterThanOrEqual.md)
- [LessThan](LessThan.md)
- [MaxAge](MaxAge.md)
- [Min](Min.md)
- [MinAge](MinAge.md)
50 changes: 50 additions & 0 deletions docs/rules/Min.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Min

- `Min(Validatable $rule)`

Validates the minimum value of the input against a given rule.

```php
v::min(v::equals(10))->validate([10, 20, 30]); // true

v::min(v::between('a', 'c'))->validate(['b', 'd', 'f']); // true

v::min(v::greaterThan(new DateTime('yesterday')))
->validate([new DateTime('today'), new DateTime('tomorrow')]); // true

v::min(v::lessThan(3))->validate([4, 8, 12]); // false
```

## Note

This rule uses PHP's [min][] function to compare the input against the given rule. The PHP manual states that:

> Values of different types will be compared using the [standard comparison rules][]. For instance, a non-numeric
> `string` will be compared to an `int` as though it were `0`, but multiple non-numeric `string` values will be compared
> alphanumerically. The actual value returned will be of the original type with no conversion applied.
## Categorization

- Aggregations
- Comparisons

## Changelog

| Version | Description |
|--------:|-----------------------------|
| 3.0.0 | Became an aggregation |
| 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)
- [LessThan](LessThan.md)
- [LessThanOrEqual](LessThanOrEqual.md)

[min]: https://www.php.net/min
[standard comparison rules]: https://www.php.net/operators.comparison
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 lessThanOrEqual(mixed $compareTo): ChainedValidator;

public function maxAge(int $age, ?string $format = null): ChainedValidator;

public function min(Validatable $rule): ChainedValidator;

public function mimetype(string $mimetype): ChainedValidator;

public function greaterThanOrEqual(mixed $compareTo): ChainedValidator;
Expand Down
64 changes: 64 additions & 0 deletions library/Rules/Min.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?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 function count;
use function is_array;
use function is_iterable;
use function iterator_to_array;
use function min;

#[Template('As the minimum from {{name}},', 'As the minimum from {{name}},')]
#[Template('The minimum from', 'The minimum from', self::TEMPLATE_NAMED)]
#[Template('{{name}} must have at least 1 item', '{{name}} must not have at least 1 item', self::TEMPLATE_EMPTY)]
#[Template(
'{{name}} must be an array or iterable to validate its minimum value',
'{{name}} must not be an array or iterable to validate its minimum value',
self::TEMPLATE_TYPE,
)]
final class Min extends Wrapper
{
public const TEMPLATE_NAMED = '__named__';
public const TEMPLATE_EMPTY = '__empty__';
public const TEMPLATE_TYPE = '__min__';

public function evaluate(mixed $input): Result
{
if (!is_iterable($input)) {
return Result::failed($input, $this);
}

$array = $this->toArray($input);
if (count($array) === 0) {
return Result::failed($input, $this);
}

$result = $this->rule->evaluate(min($array));
$template = $this->getName() === null ? self::TEMPLATE_STANDARD : self::TEMPLATE_NAMED;

return (new Result($result->isValid, $input, $this, [], $template,))->withNextSibling($result);
}

/**
* @param iterable<mixed> $input
* @return array<mixed>
*/
private function toArray(iterable $input): array
{
if (is_array($input)) {
return $input;
}

return iterator_to_array($input);
}
}
2 changes: 1 addition & 1 deletion library/Rules/Wrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ abstract class Wrapper implements Validatable
use DeprecatedValidatableMethods;

public function __construct(
private readonly Validatable $rule
protected readonly Validatable $rule
) {
}

Expand Down
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 lessThanOrEqual(mixed $compareTo): ChainedValidator;

public static function maxAge(int $age, ?string $format = null): ChainedValidator;

public static function min(Validatable $rule): ChainedValidator;

public static function mimetype(string $mimetype): ChainedValidator;

public static function greaterThanOrEqual(mixed $compareTo): ChainedValidator;
Expand Down
49 changes: 49 additions & 0 deletions tests/integration/rules/min.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
--FILE--
<?php

declare(strict_types=1);

require 'vendor/autoload.php';

use Respect\Validation\Validator as v;

run([
'Default' => [v::min(v::equals(1)), [2, 3]],
'Negative' => [v::not(v::min(v::equals(1))), [1, 2, 3]],
'With template' => [v::min(v::equals(1)), [2, 3], 'That did not go as planned'],
'With name' => [v::min(v::equals(1))->setName('Options'), [2, 3]],
]);
?>
--EXPECT--
Default
⎺⎺⎺⎺⎺⎺⎺
As the minimum from `[2, 3]`, 2 must equal 1
- As the minimum from `[2, 3]`, 2 must equal 1
[
'min' => 'As the minimum from `[2, 3]`, 2 must equal 1',
]

Negative
⎺⎺⎺⎺⎺⎺⎺⎺
As the minimum from `[1, 2, 3]`, 1 must not equal 1
- As the minimum from `[1, 2, 3]`, 1 must not equal 1
[
'min' => 'As the minimum from `[1, 2, 3]`, 1 must not equal 1',
]

With template
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
That did not go as planned
- That did not go as planned
[
'min' => 'That did not go as planned',
]

With name
⎺⎺⎺⎺⎺⎺⎺⎺⎺
The minimum from Options must equal 1
- The minimum from Options must equal 1
[
'Options' => 'The minimum from Options must equal 1',
]

48 changes: 0 additions & 48 deletions tests/library/RuleTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,6 @@
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Validatable;

use function implode;
use function ltrim;
use function realpath;
use function Respect\Stringifier\stringify;
use function sprintf;
use function strrchr;
use function substr;

abstract class RuleTestCase extends TestCase
{
/**
Expand Down Expand Up @@ -58,44 +50,4 @@ public function shouldValidateInvalidInput(Validatable $validator, mixed $input)
{
self::assertInvalidInput($validator, $input);
}

public static function fixture(?string $filename = null): string
{
$parts = [(string) realpath(__DIR__ . '/../fixtures')];
if ($filename !== null) {
$parts[] = ltrim($filename, '/');
}

return implode('/', $parts);
}

public static function assertValidInput(Validatable $rule, mixed $input): void
{
$result = $rule->evaluate($input);

self::assertTrue(
$result->isValid,
sprintf(
'%s should pass with input %s and parameters %s',
substr((string) strrchr($rule::class, '\\'), 1),
stringify($input),
stringify($result->parameters)
)
);
}

public static function assertInvalidInput(Validatable $rule, mixed $input): void
{
$result = $rule->evaluate($input);

self::assertFalse(
$result->isValid,
sprintf(
'%s should fail with input %s and parameters %s',
substr((string) strrchr($rule::class, '\\'), 1),
stringify($input),
stringify($result->parameters)
)
);
}
}
Loading

0 comments on commit cc96ee9

Please sign in to comment.