Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Min, Max, and Each to wrap chained rules. #1501

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions docs/rules/Max.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|------------|----------------|
Expand Down
7 changes: 0 additions & 7 deletions docs/rules/Min.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,6 @@ 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 |
Expand Down
49 changes: 49 additions & 0 deletions library/Rules/Core/ArrayAggregateFunction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

/*
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
* 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<mixed> $input
*/
abstract protected function extractAggregate(array $input): mixed;

/** @param non-empty-array<mixed> $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));
}
}
18 changes: 6 additions & 12 deletions library/Rules/Max.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,18 @@

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<mixed> $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);
}
}
18 changes: 6 additions & 12 deletions library/Rules/Min.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,18 @@

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 from', 'The minimum from')]
final class Min extends ArrayAggregateFunction
{
public const TEMPLATE_NAMED = '__named__';
protected string $idPrefix = 'min';

/** @param non-empty-array<mixed> $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);
}
}
9 changes: 9 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
/** @param array<string, mixed> $messages */
function expectAll(Closure $callback, string $message, string $fullMessage, array $messages): Closure
{
// Normalize newlines in $fullMessage so OS differences don't cause false failures
$fullMessage = preg_replace('/\R/u', PHP_EOL, $fullMessage);

return function () use ($callback, $message, $fullMessage, $messages): void {
try {
$callback->call($this);
Expand All @@ -30,6 +33,9 @@ function expectAll(Closure $callback, string $message, string $fullMessage, arra
/** @param array<string, mixed> $messages */
function expectAllToMatch(Closure $callback, string $message, string $fullMessage, array $messages): Closure
{
// Normalize newlines in $fullMessage so OS differences don't cause false failures
$fullMessage = preg_replace('/\R/u', PHP_EOL, $fullMessage);

return function () use ($callback, $message, $fullMessage, $messages): void {
try {
$callback();
Expand Down Expand Up @@ -60,6 +66,9 @@ function expectMessage(Closure $callback, string $message): Closure

function expectFullMessage(Closure $callback, string $fullMessage): Closure
{
// Normalize newlines in $fullMessage so OS differences don't cause false failures
$fullMessage = preg_replace('/\R/u', PHP_EOL, $fullMessage);

return function () use ($callback, $fullMessage): void {
try {
$callback();
Expand Down
52 changes: 52 additions & 0 deletions tests/feature/Rules/EachTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
]
));
27 changes: 21 additions & 6 deletions tests/feature/Rules/MaxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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',
]
));
27 changes: 21 additions & 6 deletions tests/feature/Rules/MinTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 from `[2, 3]` must be equal to 1',
'- The minimum from `[2, 3]` must be equal to 1',
['minEquals' => 'The minimum from `[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 from `[1, 2, 3]` must not be equal to 1',
'- The minimum from `[1, 2, 3]` must not be equal to 1',
['notMinEquals' => 'The minimum from `[1, 2, 3]` must not be equal to 1']
));

test('With template', expectAll(
Expand All @@ -34,3 +34,18 @@
'- The minimum from Options must be equal to 1',
['minEquals' => 'The minimum from 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 from `[2, 3, 4]` must be between 5 and 7',
<<<'FULL_MESSAGE'
- All of the required rules must pass for `[2, 3, 4]`
- The minimum from `[2, 3, 4]` must be between 5 and 7
- The minimum from `[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 from `[2, 3, 4]` must be between 5 and 7',
'minOdd' => 'The minimum from `[2, 3, 4]` must be an odd number',
]
));
12 changes: 6 additions & 6 deletions tests/feature/Transformers/PrefixTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 from `[1, 2, 3]` must be an even number',
'- The minimum from `[1, 2, 3]` must be an even number',
['minEven' => 'The minimum from `[1, 2, 3]` must be an even number']
));

test('Not', expectAll(
Expand Down
Loading