mirror of
https://github.com/Respect/Validation.git
synced 2024-06-08 00:32:16 +02:00
Create "Min" rule
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 <henriquemoody@gmail.com>
This commit is contained in:
parent
2f12b6c8d8
commit
cc96ee9102
|
@ -1,5 +1,9 @@
|
|||
# List of rules by category
|
||||
|
||||
## Aggregations
|
||||
|
||||
- [Min](rules/Min.md)
|
||||
|
||||
## Arrays
|
||||
|
||||
- [ArrayType](rules/ArrayType.md)
|
||||
|
@ -51,6 +55,7 @@
|
|||
- [In](rules/In.md)
|
||||
- [LessThan](rules/LessThan.md)
|
||||
- [LessThanOrEqual](rules/LessThanOrEqual.md)
|
||||
- [Min](rules/Min.md)
|
||||
|
||||
## Composite
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -36,3 +36,4 @@ See also:
|
|||
- [Length](Length.md)
|
||||
- [LessThan](LessThan.md)
|
||||
- [LessThanOrEqual](LessThanOrEqual.md)
|
||||
- [Min](Min.md)
|
||||
|
|
|
@ -30,3 +30,4 @@ See also:
|
|||
- [Between](Between.md)
|
||||
- [GreaterThanOrEqual](GreaterThanOrEqual.md)
|
||||
- [LessThanOrEqual](LessThanOrEqual.md)
|
||||
- [Min](Min.md)
|
||||
|
|
|
@ -37,4 +37,5 @@ See also:
|
|||
- [LessThan](LessThan.md)
|
||||
- [LessThanOrEqual](LessThanOrEqual.md)
|
||||
- [MaxAge](MaxAge.md)
|
||||
- [Min](Min.md)
|
||||
- [MinAge](MinAge.md)
|
||||
|
|
|
@ -30,3 +30,4 @@ See also:
|
|||
- [Between](Between.md)
|
||||
- [GreaterThanOrEqual](GreaterThanOrEqual.md)
|
||||
- [LessThanOrEqual](LessThanOrEqual.md)
|
||||
- [Min](Min.md)
|
||||
|
|
|
@ -36,4 +36,5 @@ See also:
|
|||
- [GreaterThanOrEqual](GreaterThanOrEqual.md)
|
||||
- [LessThan](LessThan.md)
|
||||
- [MaxAge](MaxAge.md)
|
||||
- [Min](Min.md)
|
||||
- [MinAge](MinAge.md)
|
||||
|
|
50
docs/rules/Min.md
Normal file
50
docs/rules/Min.md
Normal file
|
@ -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
|
|
@ -198,6 +198,8 @@ interface ChainedValidator extends Validatable
|
|||
|
||||
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;
|
||||
|
|
64
library/Rules/Min.php
Normal file
64
library/Rules/Min.php
Normal file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
|
||||
* 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);
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ abstract class Wrapper implements Validatable
|
|||
use DeprecatedValidatableMethods;
|
||||
|
||||
public function __construct(
|
||||
private readonly Validatable $rule
|
||||
protected readonly Validatable $rule
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
|
@ -200,6 +200,8 @@ interface StaticValidator
|
|||
|
||||
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;
|
||||
|
|
49
tests/integration/rules/min.phpt
Normal file
49
tests/integration/rules/min.phpt
Normal file
|
@ -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',
|
||||
]
|
||||
|
|
@ -13,14 +13,6 @@ use PHPUnit\Framework\Attributes\DataProvider;
|
|||
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
|
||||
{
|
||||
/**
|
||||
|
@ -58,44 +50,4 @@ abstract class RuleTestCase extends TestCase
|
|||
{
|
||||
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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,19 @@ declare(strict_types=1);
|
|||
|
||||
namespace Respect\Validation\Test;
|
||||
|
||||
use ArrayObject;
|
||||
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
|
||||
use Respect\Validation\Validatable;
|
||||
use stdClass;
|
||||
|
||||
use function array_merge;
|
||||
use function implode;
|
||||
use function ltrim;
|
||||
use function realpath;
|
||||
use function Respect\Stringifier\stringify;
|
||||
use function sprintf;
|
||||
use function strrchr;
|
||||
use function substr;
|
||||
use function tmpfile;
|
||||
|
||||
use const PHP_INT_MAX;
|
||||
|
@ -20,12 +29,55 @@ use const PHP_INT_MIN;
|
|||
|
||||
abstract class TestCase extends PHPUnitTestCase
|
||||
{
|
||||
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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<array{mixed}> */
|
||||
public static function providerForAnyValues(): array
|
||||
{
|
||||
return array_merge(
|
||||
self::providerForStringValues(),
|
||||
self::providerForNonScalarValues(),
|
||||
self::providerForEmptyIterableValues(),
|
||||
self::providerForNonEmptyIterableValues(),
|
||||
self::providerForNonIterableValues(),
|
||||
self::providerForIntegerValues(),
|
||||
self::providerForBooleanValues(),
|
||||
self::providerForFloatValues(),
|
||||
|
@ -46,15 +98,48 @@ abstract class TestCase extends PHPUnitTestCase
|
|||
/** @return array<array{mixed}> */
|
||||
public static function providerForNonScalarValues(): array
|
||||
{
|
||||
return [
|
||||
return self::providerForNonEmptyIterableValues() + self::providerForNonEmptyIterableValues() + [
|
||||
'closure' => [static fn() => 'foo'],
|
||||
'array' => [[]],
|
||||
'object' => [new stdClass()],
|
||||
'stdClass' => [new stdClass()],
|
||||
'null' => [null],
|
||||
'resource' => [tmpfile()],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<array{mixed}> */
|
||||
public static function providerForNonIterableValues(): array
|
||||
{
|
||||
return array_merge(
|
||||
self::providerForScalarValues(),
|
||||
[
|
||||
'closure' => [static fn() => 'foo'],
|
||||
'stdClass' => [new stdClass()],
|
||||
'null' => [null],
|
||||
'resource' => [tmpfile()],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<array{mixed}> */
|
||||
public static function providerForNonEmptyIterableValues(): array
|
||||
{
|
||||
return [
|
||||
'ArrayObject' => [new ArrayObject([1, 2, 3])],
|
||||
'array' => [[4, 5, 6]],
|
||||
'generator' => [(static fn() => yield 7)()], // phpcs:ignore
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<array{mixed}> */
|
||||
public static function providerForEmptyIterableValues(): array
|
||||
{
|
||||
return [
|
||||
'empty ArrayObject' => [new ArrayObject([])],
|
||||
'empty array' => [[]],
|
||||
'empty generator' => [(static fn() => yield from [])()],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<array{string}> */
|
||||
public static function providerForStringValues(): array
|
||||
{
|
||||
|
|
85
tests/unit/Rules/MinTest.php
Normal file
85
tests/unit/Rules/MinTest.php
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
|
||||
* 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(Min::class)]
|
||||
final class MinTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
#[DataProvider('providerForNonIterableValues')]
|
||||
public function itShouldInvalidateNonIterableValues(mixed $input): void
|
||||
{
|
||||
$rule = new Min(Stub::daze());
|
||||
|
||||
self::assertInvalidInput($rule, $input);
|
||||
}
|
||||
|
||||
/** @param iterable<mixed> $input */
|
||||
#[Test]
|
||||
#[DataProvider('providerForEmptyIterableValues')]
|
||||
public function itShouldInvalidateEmptyIterableValues(iterable $input): void
|
||||
{
|
||||
$rule = new Min(Stub::daze());
|
||||
|
||||
self::assertInvalidInput($rule, $input);
|
||||
}
|
||||
|
||||
/** @param iterable<mixed> $input */
|
||||
#[Test]
|
||||
#[DataProvider('providerForNonEmptyIterableValues')]
|
||||
public function itShouldValidateNonEmptyIterableValuesWhenWrappedRulePasses(iterable $input): void
|
||||
{
|
||||
$rule = new Min(Stub::pass(1));
|
||||
|
||||
self::assertValidInput($rule, $input);
|
||||
}
|
||||
|
||||
/** @param iterable<mixed> $input */
|
||||
#[Test]
|
||||
#[DataProvider('providerForMinValues')]
|
||||
public function itShouldValidateWithTheMinimumValue(iterable $input, mixed $min): void
|
||||
{
|
||||
$wrapped = Stub::pass(1);
|
||||
|
||||
$rule = new Min($wrapped);
|
||||
$rule->evaluate($input);
|
||||
|
||||
self::assertSame($min, $wrapped->inputs[0]);
|
||||
}
|
||||
|
||||
/** @return array<string, array{iterable<mixed>, mixed}> */
|
||||
public static function providerForMinValues(): array
|
||||
{
|
||||
$yesterday = new DateTimeImmutable('yesterday');
|
||||
$today = new DateTimeImmutable('today');
|
||||
$tomorrow = new DateTimeImmutable('tomorrow');
|
||||
|
||||
return [
|
||||
'3 DateTime objects' => [[$yesterday, $today, $tomorrow], $yesterday],
|
||||
'2 DateTime objects' => [[$today, $tomorrow], $today],
|
||||
'1 DateTime objects' => [[$tomorrow], $tomorrow],
|
||||
'3 integers' => [[1, 2, 3], 1],
|
||||
'2 integers' => [[2, 3], 2],
|
||||
'1 integer' => [[3], 3],
|
||||
'3 characters' => [['a', 'b', 'c'], 'a'],
|
||||
'2 characters' => [['b', 'c'], 'b'],
|
||||
'1 character' => [['c'], 'c'],
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue