Skip to content

Commit

Permalink
Update the validation engine of the "Not" rule
Browse files Browse the repository at this point in the history
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]: fc8230a

Signed-off-by: Henrique Moody <[email protected]>
  • Loading branch information
henriquemoody committed Feb 22, 2024
1 parent 99dc872 commit 1fd60ed
Show file tree
Hide file tree
Showing 3 changed files with 20 additions and 202 deletions.
14 changes: 0 additions & 14 deletions library/NonNegatable.php

This file was deleted.

124 changes: 2 additions & 122 deletions library/Rules/Not.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,132 +9,12 @@

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();
}
}
84 changes: 18 additions & 66 deletions tests/unit/Rules/NotTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,87 +10,39 @@
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);
}

#[Test]
#[DataProvider('providerForInvalidNot')]
public function notNotHaha(Validatable $rule, mixed $input): void
{
$not = new Not($rule);

self::assertFalse($not->evaluate($input)->isValid);
}
$rule = new Not($wrapped);

#[Test]
#[DataProvider('providerForSetName')]
public function notSetName(Validatable $rule): void
{
$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],
];
self::assertEquals(
$rule->evaluate('input'),
$wrapped->evaluate('input')->withInvertedMode()
);
}

/**
* @return array<array{Validatable, mixed}>
*/
public static function providerForInvalidNot(): array
/** @return iterable<string, array{Not, mixed}> */
public static function providerForValidInput(): iterable
{
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],
];
yield 'invert fail' => [new Not(Stub::fail(1)), []];
yield 'invert success x2' => [new Not(new Not(Stub::pass(1))), []];
}

/**
* @return array<array{Validatable}>
*/
public static function providerForSetName(): array
/** @return iterable<string, array{Not, mixed}> */
public static function providerForInvalidInput(): iterable
{
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))), []];
}
}

0 comments on commit 1fd60ed

Please sign in to comment.