respect-validation/tests/feature/Validators/EachTest.php
Alexandre Gomes Gaigalas bcc60ec035 Allow empty values in iterables for All, Each, Max, Min
Now empty values are again allowed in FilteredArray-style
validators.

To solve the issue with negation, a Result attribute was
added to signal indeciseveness (when a result cannot be
reliably inverted). On such cases, we consider that result
to be valid.

For example, `v::not(v::min(v::equals(10)))` says "The
lowest value of the iterable input should not be equal 10".

If the input is empty, we cannot decide whether its minimum
is equal to 10 or not, so the validator essentially becomes
a null-op.

Users that want to ensure these validators have a valid
decidable target must use it in combination with `Length`
or other similar validators to achieve the same result.
2026-01-30 21:27:16 +00:00

303 lines
13 KiB
PHP

<?php
/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-FileContributor: Dominick Johnson
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/
declare(strict_types=1);
test('Non-iterable', catchAll(
fn() => v::each(v::intType())->assert(null),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('`null` must be iterable')
->and($fullMessage)->toBe('- `null` must be iterable')
->and($messages)->toBe(['each' => '`null` must be iterable']),
));
test('Default', catchAll(
fn() => v::each(v::intType())->assert(['a', 'b', 'c']),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('`.0` must be an integer')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- Each item in `["a", "b", "c"]` must be valid
- `.0` must be an integer
- `.1` must be an integer
- `.2` must be an integer
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'Each item in `["a", "b", "c"]` must be valid',
0 => '`.0` must be an integer',
1 => '`.1` must be an integer',
2 => '`.2` must be an integer',
]),
));
test('Inverted', catchAll(
fn() => v::not(v::each(v::intType()))->assert([1, 2, 3]),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('`.0` must not be an integer')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- Each item in `[1, 2, 3]` must be invalid
- `.0` must not be an integer
- `.1` must not be an integer
- `.2` must not be an integer
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'Each item in `[1, 2, 3]` must be invalid',
0 => '`.0` must not be an integer',
1 => '`.1` must not be an integer',
2 => '`.2` must not be an integer',
]),
));
test('With name, non-iterable', catchAll(
fn() => v::each(v::named('Wrapped', v::intType()))->assert(null),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('`null` must be iterable')
->and($fullMessage)->toBe('- `null` must be iterable')
->and($messages)->toBe(['each' => '`null` must be iterable']),
));
test('With name, default', catchAll(
fn() => v::named('Outer', v::each(v::named('Inner', v::intType())))->assert(['a', 'b', 'c']),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('`.0` (<- Inner) must be an integer')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- Each item in Outer must be valid
- `.0` (<- Inner) must be an integer
- `.1` (<- Inner) must be an integer
- `.2` (<- Inner) must be an integer
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'Each item in Outer must be valid',
0 => '`.0` (<- Inner) must be an integer',
1 => '`.1` (<- Inner) must be an integer',
2 => '`.2` (<- Inner) must be an integer',
]),
));
test('With name, inverted', catchAll(
fn() => v::named('Not', v::not(v::named('Outer', v::each(v::named('Inner', v::intType())))))->assert([1, 2, 3]),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('`.0` (<- Inner) must not be an integer')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- Each item in Outer must be invalid
- `.0` (<- Inner) must not be an integer
- `.1` (<- Inner) must not be an integer
- `.2` (<- Inner) must not be an integer
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'Each item in Outer must be invalid',
0 => '`.0` (<- Inner) must not be an integer',
1 => '`.1` (<- Inner) must not be an integer',
2 => '`.2` (<- Inner) must not be an integer',
]),
));
test('With wrapper name, default', catchAll(
fn() => v::named('Wrapper', v::each(v::intType()))->assert(['a', 'b', 'c']),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('`.0` (<- Wrapper) must be an integer')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- Each item in Wrapper must be valid
- `.0` must be an integer
- `.1` must be an integer
- `.2` must be an integer
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'Each item in Wrapper must be valid',
0 => '`.0` must be an integer',
1 => '`.1` must be an integer',
2 => '`.2` must be an integer',
]),
));
test('With wrapper name, inverted', catchAll(
fn() => v::named('Not', v::not(v::named('Wrapper', v::each(v::intType()))))->assert([1, 2, 3]),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('`.0` (<- Wrapper) must not be an integer')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- Each item in Wrapper must be invalid
- `.0` must not be an integer
- `.1` must not be an integer
- `.2` must not be an integer
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'Each item in Wrapper must be invalid',
0 => '`.0` must not be an integer',
1 => '`.1` must not be an integer',
2 => '`.2` must not be an integer',
]),
));
test('With Not name, inverted', catchAll(
fn() => v::named('Not', v::not(v::each(v::intType())))->assert([1, 2, 3]),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('`.0` (<- Not) must not be an integer')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- Each item in Not must be invalid
- `.0` must not be an integer
- `.1` must not be an integer
- `.2` must not be an integer
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'Each item in Not must be invalid',
0 => '`.0` must not be an integer',
1 => '`.1` must not be an integer',
2 => '`.2` must not be an integer',
]),
));
test('With template, non-iterable', catchAll(
fn() => v::templated('You should have passed an iterable', v::each(v::intType()))->assert(null),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('You should have passed an iterable')
->and($fullMessage)->toBe('- You should have passed an iterable')
->and($messages)->toBe(['each' => 'You should have passed an iterable']),
));
test('With template, default', catchAll(
fn() => v::templated('All items should have been integers', v::each(v::intType()))
->assert(['a', 'b', 'c']),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('All items should have been integers')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- All items should have been integers
- `.0` must be an integer
- `.1` must be an integer
- `.2` must be an integer
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'All items should have been integers',
0 => '`.0` must be an integer',
1 => '`.1` must be an integer',
2 => '`.2` must be an integer',
]),
));
test('with template, inverted', catchAll(
fn() => v::templated('All items should not have been integers', v::not(v::each(v::intType())))
->assert([1, 2, 3]),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('All items should not have been integers')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- All items should not have been integers
- `.0` must not be an integer
- `.1` must not be an integer
- `.2` must not be an integer
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'All items should not have been integers',
0 => '`.0` must not be an integer',
1 => '`.1` must not be an integer',
2 => '`.2` must not be an integer',
]),
));
test('With array template, default', catchAll(
fn() => v::each(v::intType())
->assert(['a', 'b', 'c'], [
'__root__' => 'Here a sequence of items that did not pass the validation',
0 => 'First item should have been an integer',
1 => 'Second item should have been an integer',
2 => 'Third item should have been an integer',
]),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('First item should have been an integer')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- Here a sequence of items that did not pass the validation
- First item should have been an integer
- Second item should have been an integer
- Third item should have been an integer
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'Here a sequence of items that did not pass the validation',
0 => 'First item should have been an integer',
1 => 'Second item should have been an integer',
2 => 'Third item should have been an integer',
]),
));
test('With array template and name, default', catchAll(
fn() => v::named('Wrapper', v::each(v::named('Wrapped', v::intType())))
->assert(['a', 'b', 'c'], [
'__root__' => 'Here a sequence of items that did not pass the validation',
0 => 'First item should have been an integer',
1 => 'Second item should have been an integer',
2 => 'Third item should have been an integer',
]),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('First item should have been an integer')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- Here a sequence of items that did not pass the validation
- First item should have been an integer
- Second item should have been an integer
- Third item should have been an integer
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'Here a sequence of items that did not pass the validation',
0 => 'First item should have been an integer',
1 => 'Second item should have been an integer',
2 => 'Third item should have been an integer',
]),
));
test('Chained wrapped rule', catchAll(
fn() => v::each(v::between(5, 7)->odd())->assert([2, 4]),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('`.0` must be between 5 and 7')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- Each item in `[2, 4]` must be valid
- `.0` must pass all the rules
- `.0` must be between 5 and 7
- `.0` must be an odd number
- `.1` must pass all the rules
- `.1` must be between 5 and 7
- `.1` must be an odd number
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'Each item in `[2, 4]` must be valid',
0 => [
'__root__' => '`.0` must pass all the rules',
0 => '`.0` must be between 5 and 7',
1 => '`.0` must be an odd number',
],
1 => [
'__root__' => '`.1` must pass all the rules',
0 => '`.1` must be between 5 and 7',
1 => '`.1` must be an odd number',
],
]),
));
test('Multiple nested rules', catchAll(
fn() => v::each(v::arrayType()->key('my_int', v::intType()->odd())->length(v::equals(1)))->assert([['not_int' => 'wrong'], ['my_int' => 2], 'not an array']),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('`.0.my_int` must be present')
->and($fullMessage)->toBe(<<<'FULL_MESSAGE'
- Each item in `[["not_int": "wrong"], ["my_int": 2], "not an array"]` must be valid
- `.0` must pass the rules
- `.0.my_int` must be present
- `.1` must pass the rules
- `.1.my_int` must be an odd number
- `.2` must pass all the rules
- `.2` must be an array
- `.2.my_int` must be present
- The length of `.2` must be equal to 1
FULL_MESSAGE)
->and($messages)->toBe([
'__root__' => 'Each item in `[["not_int": "wrong"], ["my_int": 2], "not an array"]` must be valid',
0 => '`.0.my_int` must be present',
1 => '`.1.my_int` must be an odd number',
2 => [
'__root__' => '`.2` must pass all the rules',
'arrayType' => '`.2` must be an array',
'my_int' => '`.2.my_int` must be present',
'lengthEquals' => 'The length of `.2` must be equal to 1',
],
]),
));