Update the validation engine of the "Not" rule

With the new validation engine, the Not rule becomes ridiculously
uncomplicated.

I didn't see the need to keep the "NonNegatable." Some rules' messages
can indeed be confusing[1], but we have way more granularity control
now.

[1]: fc8230acef

Signed-off-by: Henrique Moody <henriquemoody@gmail.com>
This commit is contained in:
Henrique Moody 2024-02-13 11:58:57 +01:00
parent 99dc8720ce
commit 1fd60edcb1
No known key found for this signature in database
GPG key ID: 221E9281655813A6
3 changed files with 21 additions and 203 deletions

View file

@ -1,14 +0,0 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation;
interface NonNegatable
{
}

View file

@ -9,132 +9,12 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Attributes\ExceptionClass;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Message\Template;
use Respect\Validation\NonNegatable;
use Respect\Validation\Result;
use Respect\Validation\Validatable;
use function array_shift;
use function count;
use function current;
use function sprintf;
#[ExceptionClass(NestedValidationException::class)]
#[Template(
'All of the required rules must pass for {{name}}',
'None of there rules must pass for {{name}}',
Not::TEMPLATE_NONE,
)]
#[Template(
'These rules must pass for {{name}}',
'These rules must not pass for {{name}}',
Not::TEMPLATE_SOME,
)]
final class Not extends AbstractRule
final class Not extends Wrapper
{
public const TEMPLATE_NONE = '__none__';
public const TEMPLATE_SOME = '__some__';
private readonly Validatable $rule;
public function __construct(Validatable $rule)
{
$this->rule = $this->extractNegatedRule($rule);
}
public function getNegatedRule(): Validatable
{
return $this->rule;
}
public function setName(string $name): static
{
$this->rule->setName($name);
return parent::setName($name);
}
public function setTemplate(string $template): static
{
$this->rule->setTemplate($template);
return parent::setTemplate($template);
}
public function validate(mixed $input): bool
{
return $this->rule->validate($input) === false;
}
public function assert(mixed $input): void
{
if ($this->validate($input)) {
return;
}
$rule = $this->rule;
if ($rule instanceof AllOf) {
$rule = $this->absorbAllOf($rule, $input);
}
$exception = $rule->reportError($input);
$exception->updateMode(ValidationException::MODE_NEGATIVE);
throw $exception;
}
public function evaluate(mixed $input): Result
{
return $this->rule->evaluate($input)->withInvertedMode();
}
private function absorbAllOf(AllOf $rule, mixed $input): Validatable
{
$rules = $rule->getRules();
while (($current = array_shift($rules))) {
$rule = $current;
if (!$rule instanceof AllOf) {
continue;
}
if (!$rule->validate($input)) {
continue;
}
$rules = $rule->getRules();
}
return $rule;
}
private function extractNegatedRule(Validatable $rule): Validatable
{
if ($rule instanceof NonNegatable) {
throw new ComponentException(
sprintf(
'"%s" can not be wrapped in Not()',
$rule::class
)
);
}
if ($rule instanceof self && $rule->getNegatedRule() instanceof self) {
return $this->extractNegatedRule($rule->getNegatedRule()->getNegatedRule());
}
if (!$rule instanceof AllOf) {
return $rule;
}
$rules = $rule->getRules();
if (count($rules) === 1) {
return $this->extractNegatedRule(current($rules));
}
return $rule;
return parent::evaluate($input)->withInvertedMode();
}
}

View file

@ -10,87 +10,39 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\TestCase;
use Respect\Validation\Validatable;
use Respect\Validation\Validator;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\RuleTestCase;
#[Group('rule')]
#[CoversClass(Not::class)]
final class NotTest extends TestCase
final class NotTest extends RuleTestCase
{
#[Test]
#[DataProvider('providerForValidNot')]
public function not(Validatable $rule, mixed $input): void
public function shouldInvertTheResultOfWrappedRule(): void
{
$not = new Not($rule);
$wrapped = Stub::fail(2);
self::assertTrue($not->evaluate($input)->isValid);
$rule = new Not($wrapped);
self::assertEquals(
$rule->evaluate('input'),
$wrapped->evaluate('input')->withInvertedMode()
);
}
#[Test]
#[DataProvider('providerForInvalidNot')]
public function notNotHaha(Validatable $rule, mixed $input): void
/** @return iterable<string, array{Not, mixed}> */
public static function providerForValidInput(): iterable
{
$not = new Not($rule);
self::assertFalse($not->evaluate($input)->isValid);
yield 'invert fail' => [new Not(Stub::fail(1)), []];
yield 'invert success x2' => [new Not(new Not(Stub::pass(1))), []];
}
#[Test]
#[DataProvider('providerForSetName')]
public function notSetName(Validatable $rule): void
/** @return iterable<string, array{Not, mixed}> */
public static function providerForInvalidInput(): iterable
{
$not = new Not($rule);
$not->setName('Foo');
self::assertEquals('Foo', $not->getName());
self::assertEquals('Foo', $not->getNegatedRule()->getName());
}
/**
* @return array<array{Validatable, mixed}>
*/
public static function providerForValidNot(): array
{
return [
[new IntVal(), ''],
[new IntVal(), 'aaa'],
[new AllOf(new NoWhitespace(), new Digit()), 'as df'],
[new AllOf(new NoWhitespace(), new Digit()), '12 34'],
[new AllOf(new AllOf(new NoWhitespace(), new Digit())), '12 34'],
[new AllOf(new NoneOf(new NumericVal(), new IntVal())), 13.37],
[new NoneOf(new NumericVal(), new IntVal()), 13.37],
[Validator::noneOf(Validator::numericVal(), Validator::intVal()), 13.37],
];
}
/**
* @return array<array{Validatable, mixed}>
*/
public static function providerForInvalidNot(): array
{
return [
[new IntVal(), 123],
[new AllOf(new AnyOf(new NumericVal(), new IntVal())), 13.37],
[new AnyOf(new NumericVal(), new IntVal()), 13.37],
[Validator::anyOf(Validator::numericVal(), Validator::intVal()), 13.37],
];
}
/**
* @return array<array{Validatable}>
*/
public static function providerForSetName(): array
{
return [
'non-allOf' => [new IntVal()],
'allOf' => [new AllOf(new NumericVal(), new IntVal())],
'not' => [new Not(new Not(new IntVal()))],
'allOf with name' => [Validator::intVal()->setName('Bar')],
'noneOf' => [Validator::noneOf(Validator::numericVal(), Validator::intVal())],
];
yield 'invert pass' => [new Not(Stub::pass(1)), []];
yield 'invert fail x2' => [new Not(new Not(Stub::fail(1))), []];
}
}