Update the validation engine of composite-based rules

This change will also make the composite-based rules require at least
two rules in their constructor because those rules do not make sense
with only one rule.

Signed-off-by: Henrique Moody <henriquemoody@gmail.com>
This commit is contained in:
Henrique Moody 2024-02-22 21:11:45 +01:00
parent 41245f663f
commit c04034c2a4
No known key found for this signature in database
GPG key ID: 221E9281655813A6
26 changed files with 232 additions and 558 deletions

View file

@ -1,6 +1,6 @@
# AllOf
- `AllOf(Validatable ...$rule)`
- `AllOf(Validatable $rule1, Validatable $rule2, Validatable ...$rule)`
Will validate if all inner validators validates.
@ -17,6 +17,7 @@ v::allOf(v::intVal(), v::positive())->validate(15); // true
Version | Description
--------|-------------
3.0.0 | Require at least two rules to be passed
0.3.9 | Created
***

View file

@ -1,6 +1,6 @@
# AnyOf
- `AnyOf(Validatable ...$rule)`
- `AnyOf(Validatable $rule1, Validatable $rule2, Validatable ...$rule)`
This is a group validator that acts as an OR operator.
@ -22,6 +22,7 @@ so `AnyOf()` returns true.
Version | Description
--------|-------------
3.0.0 | Require at least two rules to be passed
2.0.0 | Created
***

View file

@ -1,6 +1,6 @@
# KeySet
- `KeySet(Key ...$rule)`
- `KeySet(Key $rule, Key ...$rules)`
Validates a keys in a defined structure.
@ -57,6 +57,7 @@ The keys' order is not considered in the validation.
Version | Description
--------|-------------
3.0.0 | Require at one rule to be passed
2.3.0 | KeySet is NonNegatable, fixed message with extra keys
1.0.0 | Created

View file

@ -1,6 +1,6 @@
# NoneOf
- `NoneOf(Validatable ...$rule)`
- `NoneOf(Validatable $rule1, Validatable $rule2, Validatable ...$rule)`
Validates if NONE of the given validators validate:
@ -22,6 +22,7 @@ In the sample above, 'foo' isn't a integer nor a float, so noneOf returns true.
Version | Description
--------|-------------
3.0.0 | Require at least two rules to be passed
0.3.9 | Created
***

View file

@ -1,6 +1,6 @@
# OneOf
- `OneOf(Validatable ...$rule)`
- `OneOf(Validatable $rule1, Validatable $rule2, Validatable ...$rule)`
Will validate if exactly one inner validator passes.
@ -23,7 +23,7 @@ character, one or the other, but not neither nor both.
Version | Description
--------|-------------
2.0.0 | Changed to pass if only one inner validator passes
3.0.0 | Require at least two rules to be passed
0.3.9 | Created
***

View file

@ -10,11 +10,10 @@ declare(strict_types=1);
namespace Respect\Validation;
use finfo;
use Respect\Validation\Rules\Key;
interface ChainedValidator extends Validatable
{
public function allOf(Validatable ...$rule): ChainedValidator;
public function allOf(Validatable $rule1, Validatable $rule2, Validatable ...$rule): ChainedValidator;
public function alnum(string ...$additionalChars): ChainedValidator;
@ -24,7 +23,7 @@ interface ChainedValidator extends Validatable
public function alwaysValid(): ChainedValidator;
public function anyOf(Validatable ...$rule): ChainedValidator;
public function anyOf(Validatable $rule1, Validatable $rule2, Validatable ...$rule): ChainedValidator;
public function arrayType(): ChainedValidator;
@ -66,9 +65,7 @@ interface ChainedValidator extends Validatable
public function contains(mixed $containsValue, bool $identical = false): ChainedValidator;
/**
* @param mixed[] $needles
*/
/** @param non-empty-array<mixed> $needles */
public function containsAny(array $needles, bool $strictCompareArray = false): ChainedValidator;
public function countable(): ChainedValidator;
@ -176,7 +173,7 @@ interface ChainedValidator extends Validatable
bool $mandatory = true
): ChainedValidator;
public function keySet(Key ...$rule): ChainedValidator;
public function keySet(Validatable $rule, Validatable ...$rules): ChainedValidator;
public function lazyConsecutive(callable $ruleCreator, callable ...$ruleCreators): ChainedValidator;
@ -219,7 +216,7 @@ interface ChainedValidator extends Validatable
public function no(bool $useLocale = false): ChainedValidator;
public function noneOf(Validatable ...$rule): ChainedValidator;
public function noneOf(Validatable $rule1, Validatable $rule2, Validatable ...$rule): ChainedValidator;
public function not(Validatable $rule): ChainedValidator;
@ -245,7 +242,7 @@ interface ChainedValidator extends Validatable
public function odd(): ChainedValidator;
public function oneOf(Validatable ...$rule): ChainedValidator;
public function oneOf(Validatable $rule1, Validatable $rule2, Validatable ...$rule): ChainedValidator;
public function optional(Validatable $rule): ChainedValidator;

View file

@ -1,121 +0,0 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Validatable;
abstract class AbstractComposite extends AbstractRule
{
/**
* @var Validatable[]
*/
private array $rules = [];
public function __construct(Validatable ...$rules)
{
$this->rules = $rules;
}
public function setName(string $name): static
{
$parentName = $this->getName();
foreach ($this->rules as $rule) {
$ruleName = $rule->getName();
if ($ruleName && $parentName !== $ruleName) {
continue;
}
$rule->setName($name);
}
return parent::setName($name);
}
public function addRule(Validatable $rule): self
{
if ($this->shouldHaveNameOverwritten($rule) && $this->getName() !== null) {
$rule->setName($this->getName());
}
$this->rules[] = $rule;
return $this;
}
/**
* @return Validatable[]
*/
public function getRules(): array
{
return $this->rules;
}
public function assert(mixed $input): void
{
$exceptions = $this->getAllThrownExceptions($input);
if (empty($exceptions)) {
return;
}
$exception = $this->reportError($input);
if ($exception instanceof NestedValidationException) {
$exception->addChildren($exceptions);
}
throw $exception;
}
/**
* @return ValidationException[]
*/
private function getAllThrownExceptions(mixed $input): array
{
$exceptions = [];
foreach ($this->getRules() as $rule) {
try {
$rule->assert($input);
} catch (ValidationException $exception) {
$this->updateExceptionTemplate($exception);
$exceptions[] = $exception;
}
}
return $exceptions;
}
private function shouldHaveNameOverwritten(Validatable $rule): bool
{
return $this->hasName($this) && !$this->hasName($rule);
}
private function hasName(Validatable $rule): bool
{
return $rule->getName() !== null;
}
private function updateExceptionTemplate(ValidationException $exception): void
{
if ($this->getTemplate() === null || $exception->hasCustomTemplate()) {
return;
}
$exception->updateTemplate($this->getTemplate());
if (!$exception instanceof NestedValidationException) {
return;
}
foreach ($exception->getChildren() as $childException) {
$this->updateExceptionTemplate($childException);
}
}
}

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Attributes\ExceptionClass;
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rule;
@ -20,7 +18,6 @@ use function array_map;
use function array_reduce;
use function count;
#[ExceptionClass(NestedValidationException::class)]
#[Template(
'These rules must pass for {{name}}',
'These rules must not pass for {{name}}',
@ -31,7 +28,7 @@ use function count;
'None of these rules must pass for {{name}}',
self::TEMPLATE_NONE,
)]
final class AllOf extends AbstractComposite
final class AllOf extends Composite
{
public const TEMPLATE_NONE = '__none__';
public const TEMPLATE_SOME = '__some__';
@ -48,40 +45,4 @@ final class AllOf extends AbstractComposite
return (new Result($valid, $input, $this, $template))->withChildren(...$children);
}
public function assert(mixed $input): void
{
try {
parent::assert($input);
} catch (NestedValidationException $exception) {
if (count($exception->getChildren()) === count($this->getRules()) && !$exception->hasCustomTemplate()) {
$exception->updateTemplate(self::TEMPLATE_NONE);
}
throw $exception;
}
}
public function check(mixed $input): void
{
foreach ($this->getRules() as $rule) {
$rule->check($input);
}
}
public function validate(mixed $input): bool
{
foreach ($this->getRules() as $rule) {
if (!$rule->validate($input)) {
return false;
}
}
return true;
}
protected function getStandardTemplate(mixed $input): string
{
return self::TEMPLATE_SOME;
}
}

View file

@ -9,23 +9,18 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Attributes\ExceptionClass;
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rule;
use function array_map;
use function array_reduce;
use function count;
#[ExceptionClass(NestedValidationException::class)]
#[Template(
'At least one of these rules must pass for {{name}}',
'At least one of these rules must not pass for {{name}}',
)]
final class AnyOf extends AbstractComposite
final class AnyOf extends Composite
{
public function evaluate(mixed $input): Result
{
@ -34,47 +29,4 @@ final class AnyOf extends AbstractComposite
return (new Result($valid, $input, $this))->withChildren(...$children);
}
public function assert(mixed $input): void
{
try {
parent::assert($input);
} catch (NestedValidationException $exception) {
if (count($exception->getChildren()) === count($this->getRules())) {
throw $exception;
}
}
}
public function validate(mixed $input): bool
{
foreach ($this->getRules() as $v) {
if ($v->validate($input)) {
return true;
}
}
return false;
}
public function check(mixed $input): void
{
foreach ($this->getRules() as $v) {
try {
$v->check($input);
return;
} catch (ValidationException $e) {
if (!isset($firstException)) {
$firstException = $e;
}
}
}
if (isset($firstException)) {
throw $firstException;
}
throw $this->reportError($input);
}
}

View file

@ -0,0 +1,70 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Helpers\DeprecatedValidatableMethods;
use Respect\Validation\Validatable;
use function array_merge;
abstract class Composite implements Validatable
{
use DeprecatedValidatableMethods;
/** @var non-empty-array<Validatable> */
private readonly array $rules;
private ?string $name = null;
private ?string $template = null;
public function __construct(Validatable $rule1, Validatable $rule2, Validatable ...$rules)
{
$this->rules = array_merge([$rule1, $rule2], $rules);
}
/** @return non-empty-array<Validatable> */
public function getRules(): array
{
return $this->rules;
}
public function setName(string $name): static
{
foreach ($this->getRules() as $rule) {
if ($rule->getName() && $this->name !== $rule->getName()) {
continue;
}
$rule->setName($name);
}
$this->name = $name;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setTemplate(string $template): static
{
$this->template = $template;
return $this;
}
public function getTemplate(): ?string
{
return $this->template;
}
}

View file

@ -9,9 +9,11 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Exceptions\InvalidRuleConstructorException;
use Respect\Validation\Message\Template;
use function array_map;
use function count;
#[Template(
'{{name}} must contain at least one of the values {{needles}}',
@ -20,13 +22,19 @@ use function array_map;
final class ContainsAny extends Envelope
{
/**
* @param mixed[] $needles At least one of the values provided must be found in input string or array
* @param non-empty-array<mixed> $needles At least one of the values provided must be found in input string or array
* @param bool $identical Defines whether the value should be compared strictly, when validating array
*/
public function __construct(array $needles, bool $identical = false)
{
// @phpstan-ignore-next-line
if (empty($needles)) {
throw new InvalidRuleConstructorException('At least one value must be provided');
}
$rules = $this->getRules($needles, $identical);
parent::__construct(
new AnyOf(...$this->getRules($needles, $identical)),
count($rules) === 1 ? $rules[0] : new AnyOf(...$rules),
['needles' => $needles]
);
}

View file

@ -18,6 +18,7 @@ use Respect\Validation\Validatable;
use function array_diff;
use function array_keys;
use function array_map;
use function array_merge;
use function array_values;
use function count;
@ -42,14 +43,14 @@ final class KeySet extends Wrapper
/** @var array<string|int> */
private readonly array $keys;
public function __construct(Validatable ...$rules)
public function __construct(Validatable $rule, Validatable ...$rules)
{
/** @var array<Key> $keyRules */
$keyRules = $this->extractMany($rules, Key::class);
$keyRules = $this->extractMany(array_merge([$rule], $rules), Key::class);
$this->keys = array_map(static fn(Key $rule) => $rule->getReference(), $keyRules);
$this->keys = array_map(static fn(Key $keyRule) => $keyRule->getReference(), $keyRules);
parent::__construct(new AllOf(...$keyRules));
parent::__construct(count($keyRules) === 1 ? $keyRules[0] : new AllOf(...$keyRules));
}
public function evaluate(mixed $input): Result

View file

@ -9,22 +9,18 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Attributes\ExceptionClass;
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rule;
use function array_map;
use function array_reduce;
use function count;
#[ExceptionClass(NestedValidationException::class)]
#[Template(
'None of these rules must pass for {{name}}',
'All of these rules must pass for {{name}}',
)]
final class NoneOf extends AbstractComposite
final class NoneOf extends Composite
{
public function evaluate(mixed $input): Result
{
@ -33,26 +29,4 @@ final class NoneOf extends AbstractComposite
return (new Result($valid, $input, $this))->withChildren(...$children);
}
public function assert(mixed $input): void
{
try {
parent::assert($input);
} catch (NestedValidationException $exception) {
if (count($exception->getChildren()) !== count($this->getRules())) {
throw $exception;
}
}
}
public function validate(mixed $input): bool
{
foreach ($this->getRules() as $rule) {
if ($rule->validate($input)) {
return false;
}
}
return true;
}
}

View file

@ -9,24 +9,18 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Attributes\ExceptionClass;
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rule;
use function array_map;
use function array_reduce;
use function array_shift;
use function count;
#[ExceptionClass(NestedValidationException::class)]
#[Template(
'Only one of these rules must pass for {{name}}',
'Only one of these rules must not pass for {{name}}',
)]
final class OneOf extends AbstractComposite
final class OneOf extends Composite
{
public function evaluate(mixed $input): Result
{
@ -35,50 +29,4 @@ final class OneOf extends AbstractComposite
return (new Result($count === 1, $input, $this))->withChildren(...$children);
}
public function assert(mixed $input): void
{
try {
parent::assert($input);
} catch (NestedValidationException $exception) {
if (count($exception->getChildren()) !== count($this->getRules()) - 1) {
throw $exception;
}
}
}
public function validate(mixed $input): bool
{
$rulesPassedCount = 0;
foreach ($this->getRules() as $rule) {
if (!$rule->validate($input)) {
continue;
}
++$rulesPassedCount;
}
return $rulesPassedCount === 1;
}
public function check(mixed $input): void
{
$exceptions = [];
$rulesPassedCount = 0;
foreach ($this->getRules() as $rule) {
try {
$rule->check($input);
++$rulesPassedCount;
} catch (ValidationException $exception) {
$exceptions[] = $exception;
}
}
if ($rulesPassedCount === 1) {
return;
}
throw array_shift($exceptions) ?: $this->reportError($input);
}
}

View file

@ -13,7 +13,7 @@ use finfo;
interface StaticValidator
{
public static function allOf(Validatable ...$rule): ChainedValidator;
public static function allOf(Validatable $rule1, Validatable $rule2, Validatable ...$rule): ChainedValidator;
public static function alnum(string ...$additionalChars): ChainedValidator;
@ -23,7 +23,7 @@ interface StaticValidator
public static function alwaysValid(): ChainedValidator;
public static function anyOf(Validatable ...$rule): ChainedValidator;
public static function anyOf(Validatable $rule1, Validatable $rule2, Validatable ...$rule): ChainedValidator;
public static function arrayType(): ChainedValidator;
@ -67,9 +67,7 @@ interface StaticValidator
public static function contains(mixed $containsValue, bool $identical = false): ChainedValidator;
/**
* @param mixed[] $needles
*/
/** @param non-empty-array<mixed> $needles */
public static function containsAny(array $needles, bool $strictCompareArray = false): ChainedValidator;
public static function countable(): ChainedValidator;
@ -177,7 +175,7 @@ interface StaticValidator
bool $mandatory = true
): ChainedValidator;
public static function keySet(Validatable ...$rule): ChainedValidator;
public static function keySet(Validatable $rule, Validatable ...$rules): ChainedValidator;
public static function lazyConsecutive(callable $ruleCreator, callable ...$ruleCreators): ChainedValidator;
@ -220,7 +218,7 @@ interface StaticValidator
public static function no(bool $useLocale = false): ChainedValidator;
public static function noneOf(Validatable ...$rule): ChainedValidator;
public static function noneOf(Validatable $rule1, Validatable $rule2, Validatable ...$rule): ChainedValidator;
public static function not(Validatable $rule): ChainedValidator;
@ -246,7 +244,7 @@ interface StaticValidator
public static function odd(): ChainedValidator;
public static function oneOf(Validatable ...$rule): ChainedValidator;
public static function oneOf(Validatable $rule1, Validatable $rule2, Validatable ...$rule): ChainedValidator;
public static function optional(Validatable $rule): ChainedValidator;

View file

@ -8,7 +8,7 @@ require 'vendor/autoload.php';
use Respect\Validation\Validator as v;
$validator = v::create()
->key('age', v::intType()->notEmpty()->noneOf(v::stringType()))
->key('age', v::intType()->notEmpty()->noneOf(v::stringType(), v::arrayType()))
->key('reference', v::stringType()->notEmpty()->length(1, 50));
exceptionFullMessage(static fn() => $validator->assert(['age' => 1]));

View file

@ -8,11 +8,10 @@ require 'vendor/autoload.php';
use Respect\Validation\Validator as v;
run([
'Single rule' => [v::allOf(v::stringType()), 1],
'Two rules' => [v::allOf(v::intType(), v::negative()), '2'],
'Wrapped by "not"' => [v::not(v::allOf(v::intType(), v::positive())), 3],
'Wrapping "not"' => [v::allOf(v::not(v::intType(), v::positive())), 4],
'With a single template' => [v::allOf(v::stringType()), 5, 'This is a single template'],
'Wrapping "not"' => [v::allOf(v::not(v::intType(), v::positive()), v::greaterThan(2)), 4],
'With a single template' => [v::allOf(v::stringType(), v::arrayType()), 5, 'This is a single template'],
'With multiple templates' => [
v::allOf(v::stringType(), v::uppercase()),
5,
@ -25,14 +24,6 @@ run([
]);
?>
--EXPECT--
Single rule
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
1 must be of type string
- 1 must be of type string
[
'stringType' => '1 must be of type string',
]
Two rules
⎺⎺⎺⎺⎺⎺⎺⎺⎺
"2" must be of type integer

View file

@ -0,0 +1,21 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Test\Rules;
use Respect\Validation\Result;
use Respect\Validation\Rules\Composite;
final class ConcreteComposite extends Composite
{
public function evaluate(mixed $input): Result
{
return Result::passed($input, $this);
}
}

View file

@ -1,39 +0,0 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Test\Stubs;
use Respect\Validation\Message\Parameter\Stringify;
use Respect\Validation\Message\TemplateRenderer;
use Respect\Validation\Rules\AbstractComposite;
use Respect\Validation\Test\Exceptions\CompositeStubException;
use Respect\Validation\Validatable;
final class CompositeSub extends AbstractComposite
{
public function validate(mixed $input): bool
{
return true;
}
/**
* @param array<string, mixed> $extraParameters
*/
public function reportError(mixed $input, array $extraParameters = []): CompositeStubException
{
return new CompositeStubException(
input: $input,
id: 'CompositeStub',
params: $extraParameters,
template: Validatable::TEMPLATE_STANDARD,
templates: [],
formatter: new TemplateRenderer(static fn ($value) => $value, new Stringify())
);
}
}

View file

@ -1,180 +0,0 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\Exceptions\CompositeStubException;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\Stubs\CompositeSub;
use Respect\Validation\Test\TestCase;
use function current;
#[Group('rule')]
#[CoversClass(AbstractComposite::class)]
final class AbstractCompositeTest extends TestCase
{
#[Test]
public function itShouldUpdateTheNameOfTheChildWhenUpdatingItsName(): void
{
$ruleName = 'something';
$child = Stub::pass(1);
$parent = new CompositeSub($child);
self::assertNull($child->getName());
$parent->setName($ruleName);
self::assertSame($ruleName, $child->getName());
}
#[Test]
public function itShouldUpdateTheNameOfTheChildWhenAddingIt(): void
{
$ruleName = 'something';
$rule = Stub::pass(1);
$sut = new CompositeSub();
$sut->setName($ruleName);
self::assertNull($rule->getName());
$sut->addRule($rule);
self::assertSame($ruleName, $rule->getName());
}
#[Test]
public function itShouldNotUpdateTheNameOfTheChildWhenUpdatingItsNameIfTheChildAlreadyHasSomeName(): void
{
$ruleName1 = 'something';
$ruleName2 = 'something else';
$rule = Stub::pass(1);
$rule->setName($ruleName1);
$sut = new CompositeSub($rule);
$sut->setName($ruleName2);
self::assertSame($ruleName1, $rule->getName());
}
#[Test]
public function itNotShouldUpdateTheNameOfTheChildWhenAddingItIfTheChildAlreadyHasSomeName(): void
{
$ruleName1 = 'something';
$ruleName2 = 'something else';
$rule = Stub::pass(1);
$rule->setName($ruleName1);
$sut = new CompositeSub();
$sut->setName($ruleName2);
$sut->addRule($rule);
self::assertSame($ruleName1, $rule->getName());
}
#[Test]
public function itShouldReturnItsChildren(): void
{
$child1 = Stub::pass(1);
$child2 = Stub::pass(1);
$child3 = Stub::pass(1);
$sut = new CompositeSub($child1, $child2, $child3);
$children = $sut->getRules();
self::assertCount(3, $children);
self::assertSame($child1, $children[0]);
self::assertSame($child2, $children[1]);
self::assertSame($child3, $children[2]);
}
#[Test]
public function itShouldAssertWithAllChildrenAndNotThrowAnExceptionWhenThereAreNoIssues(): void
{
$input = 'something';
$child1 = Stub::pass(1);
$child2 = Stub::pass(1);
$child3 = Stub::pass(1);
$this->expectNotToPerformAssertions();
$sut = new CompositeSub($child1, $child2, $child3);
$sut->assert($input);
}
#[Test]
public function itShouldAssertWithAllChildrenAndThrowAnExceptionWhenThereAreIssues(): void
{
$sut = new CompositeSub(Stub::fail(1), Stub::fail(1), Stub::fail(1));
try {
$sut->assert('something');
} catch (CompositeStubException $exception) {
self::assertCount(3, $exception->getChildren());
}
}
#[Test]
public function itShouldUpdateTheTemplateOfEveryChildrenWhenAsserting(): void
{
$template = 'This is my template';
$sut = new CompositeSub(
Stub::fail(1),
Stub::fail(1),
Stub::fail(1)
);
$sut->setTemplate($template);
try {
$sut->assert('something');
} catch (CompositeStubException $exception) {
foreach ($exception->getChildren() as $child) {
self::assertEquals($template, $child->getMessage());
}
}
}
#[Test]
public function itShouldUpdateTheTemplateOfEveryTheChildrenOfSomeChildWhenAsserting(): void
{
$template = 'This is my template';
$sut = new CompositeSub(
Stub::fail(1),
Stub::fail(1),
new CompositeSub(Stub::fail(1))
);
$sut->setTemplate($template);
try {
$sut->assert('something');
} catch (CompositeStubException $exception) {
foreach ($exception->getChildren() as $child) {
self::assertEquals($template, $child->getMessage());
if (!$child instanceof CompositeStubException) {
continue;
}
self::assertNotFalse(current($child->getChildren()));
self::assertEquals($template, current($child->getChildren())->getMessage());
}
}
}
}

View file

@ -21,14 +21,13 @@ final class AllOfTest extends RuleTestCase
/** @return iterable<string, array{AllOf, mixed}> */
public static function providerForValidInput(): iterable
{
yield 'pass' => [new AllOf(Stub::pass(1)), []];
yield 'pass, pass' => [new AllOf(Stub::pass(1), Stub::pass(1)), []];
yield 'pass, pass, pass' => [new AllOf(Stub::pass(1), Stub::pass(1), Stub::pass(1)), []];
}
/** @return iterable<string, array{AllOf, mixed}> */
public static function providerForInvalidInput(): iterable
{
yield 'fail' => [new AllOf(Stub::fail(1)), []];
yield 'pass, fail' => [new AllOf(Stub::pass(1), Stub::fail(1)), []];
yield 'fail, pass' => [new AllOf(Stub::fail(1), Stub::pass(1)), []];
yield 'pass, pass, fail' => [new AllOf(Stub::pass(1), Stub::pass(1), Stub::fail(1)), []];

View file

@ -21,7 +21,6 @@ final class AnyOfTest extends RuleTestCase
/** @return iterable<string, array{AnyOf, mixed}> */
public static function providerForValidInput(): iterable
{
yield 'pass' => [new AnyOf(Stub::pass(1)), []];
yield 'fail, pass' => [new AnyOf(Stub::fail(1), Stub::pass(1)), []];
yield 'fail, fail, pass' => [new AnyOf(Stub::fail(1), Stub::fail(1), Stub::pass(1)), []];
yield 'fail, pass, fail' => [new AnyOf(Stub::fail(1), Stub::pass(1), Stub::fail(1)), []];
@ -30,7 +29,6 @@ final class AnyOfTest extends RuleTestCase
/** @return iterable<string, array{AnyOf, mixed}> */
public static function providerForInvalidInput(): iterable
{
yield 'fail' => [new AnyOf(Stub::fail(1)), []];
yield 'fail, fail' => [new AnyOf(Stub::fail(1), Stub::fail(1)), []];
yield 'fail, fail, fail' => [new AnyOf(Stub::fail(1), Stub::fail(1), Stub::fail(1)), []];
}

View file

@ -0,0 +1,84 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\Rules\ConcreteComposite;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\TestCase;
#[Group('core')]
#[CoversClass(Composite::class)]
final class CompositeTest extends TestCase
{
#[Test]
public function itShouldReturnItsChildren(): void
{
$expected = [Stub::daze(), Stub::daze(), Stub::daze()];
$sut = new ConcreteComposite(...$expected);
$actual = $sut->getRules();
self::assertCount(3, $actual);
self::assertEquals($expected, $actual);
}
#[Test]
public function itShouldDefineAndRetrieveTemplate(): void
{
$template = 'This is a template';
$sut = new ConcreteComposite(Stub::daze(), Stub::daze());
$sut->setTemplate($template);
self::assertEquals($template, $sut->getTemplate());
}
#[Test]
public function itShouldUpdateTheNameOfTheChildWhenUpdatingItsName(): void
{
$ruleName = 'something';
$rule1 = Stub::daze();
$rule2 = Stub::daze();
$composite = new ConcreteComposite($rule1, $rule2);
self::assertNull($rule1->getName());
self::assertNull($rule2->getName());
$composite->setName($ruleName);
self::assertEquals($ruleName, $rule1->getName());
self::assertEquals($ruleName, $rule2->getName());
self::assertEquals($ruleName, $composite->getName());
}
#[Test]
public function itShouldNotUpdateTheNameOfTheChildWhenUpdatingItsNameIfTheChildAlreadyHasSomeName(): void
{
$ruleName1 = 'something';
$ruleName2 = 'something else';
$rule1 = Stub::daze();
$rule1->setName($ruleName1);
$rule2 = Stub::daze();
$rule2->setName($ruleName1);
$composite = new ConcreteComposite($rule1, $rule2);
$composite->setName($ruleName2);
self::assertEquals($ruleName1, $rule1->getName());
self::assertEquals($ruleName1, $rule2->getName());
self::assertEquals($ruleName2, $composite->getName());
}
}

View file

@ -11,12 +11,24 @@ namespace Respect\Validation\Rules;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Exceptions\InvalidRuleConstructorException;
use Respect\Validation\Test\RuleTestCase;
#[Group('rule')]
#[CoversClass(ContainsAny::class)]
final class ContainsAnyTest extends RuleTestCase
{
#[Test]
public function itShouldThrowAnExceptionWhenThereAreNoNeedles(): void
{
$this->expectException(InvalidRuleConstructorException::class);
$this->expectExceptionMessage('At least one value must be provided');
// @phpstan-ignore-next-line
new ContainsAny([]);
}
/** @return iterable<array{ContainsAny, mixed}> */
public static function providerForValidInput(): iterable
{

View file

@ -21,7 +21,6 @@ final class NoneOfTest extends RuleTestCase
/** @return iterable<string, array{NoneOf, mixed}> */
public static function providerForValidInput(): iterable
{
yield 'fail' => [new NoneOf(Stub::fail(1)), []];
yield 'fail, fail' => [new NoneOf(Stub::fail(1), Stub::fail(1)), []];
yield 'fail, fail, fail' => [new NoneOf(Stub::fail(1), Stub::fail(1), Stub::fail(1)), []];
}
@ -29,7 +28,6 @@ final class NoneOfTest extends RuleTestCase
/** @return iterable<string, array{NoneOf, mixed}> */
public static function providerForInvalidInput(): iterable
{
yield 'pass' => [new NoneOf(Stub::pass(1)), []];
yield 'pass, fail' => [new NoneOf(Stub::pass(1), Stub::fail(1)), []];
yield 'fail, pass' => [new NoneOf(Stub::fail(1), Stub::pass(1)), []];
yield 'pass, pass, fail' => [new NoneOf(Stub::pass(1), Stub::pass(1), Stub::fail(1)), []];

View file

@ -21,7 +21,6 @@ final class OneOfTest extends RuleTestCase
/** @return iterable<string, array{OneOf, mixed}> */
public static function providerForValidInput(): iterable
{
yield 'pass' => [new OneOf(Stub::pass(1)), []];
yield 'fail, pass' => [new OneOf(Stub::fail(1), Stub::pass(1)), []];
yield 'pass, fail' => [new OneOf(Stub::pass(1), Stub::fail(1)), []];
yield 'pass, fail, fail' => [new OneOf(Stub::pass(1), Stub::fail(1), Stub::fail(1)), []];
@ -32,7 +31,6 @@ final class OneOfTest extends RuleTestCase
/** @return iterable<string, array{OneOf, mixed}> */
public static function providerForInvalidInput(): iterable
{
yield 'fail' => [new OneOf(Stub::fail(1)), []];
yield 'fail, fail' => [new OneOf(Stub::fail(1), Stub::fail(1)), []];
yield 'fail, fail, fail' => [new OneOf(Stub::fail(1), Stub::fail(1), Stub::fail(1)), []];
yield 'fail, pass, pass' => [new OneOf(Stub::fail(1), Stub::pass(1), Stub::pass(1)), []];