Update the validation engine of wrapper-based rules

Signed-off-by: Henrique Moody <henriquemoody@gmail.com>
This commit is contained in:
Henrique Moody 2024-02-12 23:06:51 +01:00
parent e341fef5c0
commit 99dc8720ce
No known key found for this signature in database
GPG key ID: 221E9281655813A6
12 changed files with 333 additions and 602 deletions

View file

@ -0,0 +1,61 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Helpers;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Validatable;
use Respect\Validation\Validator;
use function array_map;
use function count;
use function current;
use function sprintf;
trait CanExtractRules
{
private function extractSingle(Validatable $rule, string $class): Validatable
{
if ($rule instanceof Validator) {
return $this->extractSingleFromValidator($rule, $class);
}
if (!$rule instanceof $class) {
throw new ComponentException(sprintf(
'Could not extract rule %s from %s',
$class,
$rule::class,
));
}
return $rule;
}
/**
* @param array<Validatable> $rules
*
* @return array<Validatable>
*/
private function extractMany(array $rules, string $class): array
{
return array_map(fn (Validatable $rule) => $this->extractSingle($rule, $class), $rules);
}
private function extractSingleFromValidator(Validator $rule, string $class): Validatable
{
$rules = $rule->getRules();
if (count($rules) !== 1) {
throw new ComponentException(sprintf(
'Validator must contain exactly one rule'
));
}
return $this->extractSingle(current($rules), $class);
}
}

View file

@ -1,55 +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\Result;
use Respect\Validation\Validatable;
abstract class AbstractWrapper extends AbstractRule
{
public function __construct(
private readonly Validatable $validatable
) {
}
public function evaluate(mixed $input): Result
{
return $this->validatable->evaluate($input);
}
public function assert(mixed $input): void
{
$this->validatable->assert($input);
}
public function check(mixed $input): void
{
$this->validatable->check($input);
}
public function validate(mixed $input): bool
{
return $this->validatable->validate($input);
}
public function setName(string $name): static
{
$this->validatable->setName($name);
return parent::setName($name);
}
public function setTemplate(string $template): static
{
$this->validatable->setTemplate($template);
return parent::setTemplate($template);
}
}

View file

@ -9,153 +9,70 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Attributes\ExceptionClass;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Exceptions\NonOmissibleValidationException;
use Respect\Validation\Helpers\CanBindEvaluateRule;
use Respect\Validation\Helpers\CanExtractRules;
use Respect\Validation\Message\Template;
use Respect\Validation\NonNegatable;
use Respect\Validation\Result;
use Respect\Validation\Validatable;
use function array_key_exists;
use function array_diff;
use function array_keys;
use function array_map;
use function array_values;
use function count;
use function current;
use function is_array;
#[ExceptionClass(NonOmissibleValidationException::class)]
#[Template(
'All of the required rules must pass for {{name}}',
'',
self::TEMPLATE_NONE,
'Must have keys {{missingKeys}} in {{name}}',
'Must not have keys {{missingKeys}} in {{name}}',
self::TEMPLATE_MISSING,
)]
#[Template(
'These rules must pass for {{name}}',
'',
self::TEMPLATE_SOME,
'Must not have keys {{extraKeys}} in {{name}}',
'Must have keys {{extraKeys}} in {{name}}',
self::TEMPLATE_EXTRA,
)]
#[Template(
'Must have keys {{keys}}',
'',
self::TEMPLATE_STRUCTURE,
)]
#[Template(
'Must not have keys {{extraKeys}}',
'',
self::TEMPLATE_STRUCTURE_EXTRA,
)]
final class KeySet extends AbstractWrapper implements NonNegatable
final class KeySet extends Wrapper
{
public const TEMPLATE_NONE = '__none__';
public const TEMPLATE_SOME = '__some__';
public const TEMPLATE_STRUCTURE = '__structure__';
public const TEMPLATE_STRUCTURE_EXTRA = '__structure_extra__';
use CanBindEvaluateRule;
use CanExtractRules;
/**
* @var mixed[]
*/
public const TEMPLATE_MISSING = '__missing__';
public const TEMPLATE_EXTRA = '__extra__';
/** @var array<string|int> */
private readonly array $keys;
/**
* @var mixed[]
*/
private array $extraKeys = [];
/**
* @var Key[]
*/
private readonly array $keyRules;
public function __construct(Validatable ...$validatables)
public function __construct(Validatable ...$rules)
{
$this->keyRules = array_map([$this, 'getKeyRule'], $validatables);
$this->keys = array_map([$this, 'getKeyReference'], $this->keyRules);
/** @var array<Key> $keyRules */
$keyRules = $this->extractMany($rules, Key::class);
parent::__construct(new AllOf(...$this->keyRules));
$this->keys = array_map(static fn(Key $rule) => $rule->getReference(), $keyRules);
parent::__construct(new AllOf(...$keyRules));
}
public function assert(mixed $input): void
public function evaluate(mixed $input): Result
{
if (!$this->hasValidStructure($input)) {
throw $this->reportError($input);
$result = $this->bindEvaluate(new ArrayType(), $this, $input);
if (!$result->isValid) {
return $result;
}
parent::assert($input);
}
$inputKeys = array_keys($input);
public function check(mixed $input): void
{
if (!$this->hasValidStructure($input)) {
throw $this->reportError($input);
$missingKeys = array_diff($this->keys, $inputKeys);
if (count($missingKeys) > 0) {
return Result::failed($input, $this, self::TEMPLATE_MISSING)
->withParameters(['missingKeys' => array_values($missingKeys)]);
}
parent::check($input);
}
public function validate(mixed $input): bool
{
if (!$this->hasValidStructure($input)) {
return false;
$extraKeys = array_diff($inputKeys, $this->keys);
if (count($extraKeys) > 0) {
return Result::failed($input, $this, self::TEMPLATE_EXTRA)
->withParameters(['extraKeys' => array_values($extraKeys)]);
}
return parent::validate($input);
}
/**
* @return array<string, mixed>
*/
public function getParams(): array
{
return [
'keys' => $this->keys,
'extraKeys' => $this->extraKeys,
];
}
protected function getStandardTemplate(mixed $input): string
{
if (count($this->extraKeys)) {
return self::TEMPLATE_STRUCTURE_EXTRA;
}
return KeySet::TEMPLATE_STRUCTURE;
}
private function getKeyRule(Validatable $validatable): Key
{
if ($validatable instanceof Key) {
return $validatable;
}
if (!$validatable instanceof AllOf || count($validatable->getRules()) !== 1) {
throw new ComponentException('KeySet rule accepts only Key rules');
}
return $this->getKeyRule(current($validatable->getRules()));
}
private function getKeyReference(Key $rule): mixed
{
return $rule->getReference();
}
private function hasValidStructure(mixed $input): bool
{
if (!is_array($input)) {
return false;
}
foreach ($this->keyRules as $keyRule) {
if (!array_key_exists($keyRule->getReference(), $input) && $keyRule->isMandatory()) {
return false;
}
unset($input[$keyRule->getReference()]);
}
foreach ($input as $extraKey => &$ignoreValue) {
$this->extraKeys[] = $extraKey;
}
return count($input) == 0;
return parent::evaluate($input);
}
}

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
#[Template(
'The value must be nullable',
@ -21,43 +22,20 @@ use Respect\Validation\Message\Template;
'{{name}} must not be null',
self::TEMPLATE_NAMED,
)]
final class Nullable extends AbstractWrapper
final class Nullable extends Wrapper
{
public const TEMPLATE_NAMED = '__named__';
public function assert(mixed $input): void
public function evaluate(mixed $input): Result
{
if ($input === null) {
return;
if ($input !== null) {
return parent::evaluate($input);
}
parent::assert($input);
}
public function check(mixed $input): void
{
if ($input === null) {
return;
if ($this->getName()) {
return Result::passed($input, $this, self::TEMPLATE_NAMED);
}
parent::check($input);
}
public function validate(mixed $input): bool
{
if ($input === null) {
return true;
}
return parent::validate($input);
}
protected function getStandardTemplate(mixed $input): string
{
if ($input || $this->getName()) {
return self::TEMPLATE_NAMED;
}
return self::TEMPLATE_STANDARD;
return Result::passed($input, $this);
}
}

View file

@ -23,7 +23,7 @@ use Respect\Validation\Result;
'{{name}} must not be optional',
self::TEMPLATE_NAMED,
)]
final class Optional extends AbstractWrapper
final class Optional extends Wrapper
{
use CanValidateUndefined;
@ -31,46 +31,14 @@ final class Optional extends AbstractWrapper
public function evaluate(mixed $input): Result
{
if ($this->isUndefined($input)) {
return Result::passed($input, $this);
if (!$this->isUndefined($input)) {
return parent::evaluate($input);
}
return parent::evaluate($input);
}
public function assert(mixed $input): void
{
if ($this->isUndefined($input)) {
return;
}
parent::assert($input);
}
public function check(mixed $input): void
{
if ($this->isUndefined($input)) {
return;
}
parent::check($input);
}
public function validate(mixed $input): bool
{
if ($this->isUndefined($input)) {
return true;
}
return parent::validate($input);
}
protected function getStandardTemplate(mixed $input): string
{
if ($this->getName()) {
return self::TEMPLATE_NAMED;
return Result::passed($input, $this, self::TEMPLATE_NAMED);
}
return self::TEMPLATE_STANDARD;
return Result::passed($input, $this);
}
}

53
library/Rules/Wrapper.php Normal file
View file

@ -0,0 +1,53 @@
<?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\Result;
use Respect\Validation\Validatable;
abstract class Wrapper implements Validatable
{
use DeprecatedValidatableMethods;
public function __construct(
private readonly Validatable $rule
) {
}
public function evaluate(mixed $input): Result
{
return $this->rule->evaluate($input);
}
public function getName(): ?string
{
return $this->rule->getName();
}
public function setName(string $name): static
{
$this->rule->setName($name);
return $this;
}
public function getTemplate(): ?string
{
return $this->rule->getTemplate();
}
public function setTemplate(string $template): static
{
$this->rule->setTemplate($template);
return $this;
}
}

View file

@ -0,0 +1,48 @@
--FILE--
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use Respect\Validation\Validator as v;
$input = ['foo' => 42, 'bar' => 'String'];
$missingKeys = v::create()
->keySet(
v::key('foo', v::intVal()),
v::key('bar', v::stringType()),
v::key('baz', v::boolType())
);
$extraKeys = v::create()
->keySet(
v::key('foo', v::intVal()),
v::key('bar', v::stringType()),
);
$correctStructure = v::create()
->keySet(
v::key('foo', v::stringType()),
v::key('bar', v::intType()),
);
exceptionMessage(static fn() => $missingKeys->assert(false));
exceptionMessage(static fn() => $missingKeys->assert($input));
exceptionMessage(static fn() => $extraKeys->assert($input + ['baz' => true]));
exceptionMessage(static fn() => $correctStructure->assert($input));
exceptionFullMessage(static fn() => $missingKeys->assert(false));
exceptionFullMessage(static fn() => $missingKeys->assert($input));
exceptionFullMessage(static fn() => $extraKeys->assert($input + ['baz' => true]));
exceptionFullMessage(static fn() => $correctStructure->assert($input));
?>
--EXPECT--
`false` must be of type array
Must have keys `["baz"]` in `["foo": 42, "bar": "String"]`
Must not have keys `["baz"]` in `["foo": 42, "bar": "String", "baz": true]`
foo must be of type string
- `false` must be of type array
- Must have keys `["baz"]` in `["foo": 42, "bar": "String"]`
- Must not have keys `["baz"]` in `["foo": 42, "bar": "String", "baz": true]`
- All of the required rules must pass for `["foo": 42, "bar": "String"]`
- foo must be of type string
- bar must be of type integer

View file

@ -9,8 +9,8 @@ declare(strict_types=1);
namespace Respect\Validation\Test\Rules;
use Respect\Validation\Rules\AbstractWrapper;
use Respect\Validation\Rules\Wrapper;
final class WrapperStub extends AbstractWrapper
final class WrapperStub extends Wrapper
{
}

View file

@ -10,184 +10,27 @@ 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\Exceptions\ComponentException;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\TestCase;
use stdClass;
use function Respect\Stringifier\stringify;
use function sprintf;
use Respect\Validation\Test\RuleTestCase;
#[Group('rule')]
#[CoversClass(KeySet::class)]
final class KeySetTest extends TestCase
final class KeySetTest extends RuleTestCase
{
#[Test]
public function shouldNotAcceptAllOfWithMoreThanOneKeyRule(): void
/** @return iterable<string, array{KeySet, mixed}> */
public static function providerForValidInput(): iterable
{
$this->expectException(ComponentException::class);
$this->expectExceptionMessage('KeySet rule accepts only Key rules');
new KeySet(new AllOf(
new Key('foo', Stub::daze(), false),
new Key('bar', Stub::daze(), false),
));
yield 'correct keys, without rule' => [new KeySet(new Key('foo')), ['foo' => 'bar']];
yield 'correct keys, with passing rule' => [new KeySet(new Key('foo', Stub::pass(1))), ['foo' => 'bar']];
}
#[Test]
public function shouldNotAcceptAllOfWithNonKeyRule(): void
/** @return iterable<string, array{KeySet, mixed}> */
public static function providerForInvalidInput(): iterable
{
$this->expectException(ComponentException::class);
$this->expectExceptionMessage('KeySet rule accepts only Key rules');
new KeySet(new AllOf(Stub::daze()));
}
#[Test]
public function shouldNotAcceptNonKeyRule(): void
{
$this->expectException(ComponentException::class);
$this->expectExceptionMessage('KeySet rule accepts only Key rules');
new KeySet(Stub::daze());
}
#[Test]
public function shouldValidateKeysWhenThereAreMissingRequiredKeys(): void
{
$input = [
'foo' => 42,
];
$sut = new KeySet(
new Key('foo', Stub::pass(1), true),
new Key('bar', Stub::daze(), true),
);
self::assertFalse($sut->validate($input));
}
#[Test]
public function shouldValidateKeysWhenThereAreMissingNonRequiredKeys(): void
{
$input = [
'foo' => 42,
];
$sut = new KeySet(
new Key('foo', Stub::pass(1), true),
new Key('bar', Stub::daze(), false),
);
self::assertTrue($sut->validate($input));
}
#[Test]
public function shouldValidateKeysWhenThereAreMoreKeys(): void
{
$input = [
'foo' => 42,
'bar' => 'String',
'baz' => false,
];
$sut = new KeySet(
new Key('foo', Stub::pass(1), false),
new Key('bar', Stub::pass(1), false),
);
self::assertFalse($sut->validate($input));
}
#[Test]
public function shouldValidateKeysWhenEmpty(): void
{
$sut = new KeySet(
new Key('foo', Stub::daze(), true),
new Key('bar', Stub::daze(), true),
);
self::assertFalse($sut->validate([]));
}
#[Test]
public function shouldCheckKeys(): void
{
$sut = new KeySet(
new Key('foo', Stub::pass(1), true),
new Key('bar', Stub::pass(1), true),
);
$this->expectException(ValidationException::class);
$this->expectExceptionMessage(sprintf('Must have keys %s', stringify(['foo', 'bar'])));
$sut->check([]);
}
#[Test]
public function shouldAssertKeys(): void
{
$sut = new KeySet(
new Key('foo', Stub::pass(1), true),
new Key('bar', Stub::pass(1), true),
);
$this->expectException(ValidationException::class);
$this->expectExceptionMessage(sprintf('Must have keys %s', stringify(['foo', 'bar'])));
$sut->assert([]);
}
#[Test]
public function shouldWarnOfExtraKeysWithMessage(): void
{
$input = ['foo' => 123, 'bar' => 456];
$sut = new KeySet(new Key('foo', Stub::pass(1), true));
$this->expectException(ValidationException::class);
$this->expectExceptionMessage(sprintf('Must not have keys %s', stringify(['bar'])));
$sut->assert($input);
}
#[Test]
public function cannotBeNegated(): void
{
$key1 = new Key('foo', Stub::pass(1), true);
$this->expectException(ComponentException::class);
$this->expectExceptionMessage('"Respect\Validation\Rules\KeySet" can not be wrapped in Not()');
new Not(new KeySet($key1));
}
#[Test]
#[DataProvider('providerForInvalidArguments')]
public function shouldThrowExceptionInCaseArgumentIsAnythingOtherThanArray(mixed $input): void
{
$sut = new KeySet(new Key('name'));
$this->expectException(ValidationException::class);
$this->expectExceptionMessage(sprintf('Must have keys %s', stringify(['name'])));
$sut->assert($input);
}
/**
* @return mixed[][]
*/
public static function providerForInvalidArguments(): array
{
return [
[''],
[null],
[0],
[new stdClass()],
];
yield 'not array' => [new KeySet(new Key('foo')), null];
yield 'missing keys' => [new KeySet(new Key('foo')), []];
yield 'extra keys' => [new KeySet(new Key('foo')), ['foo' => 'bar', 'baz' => 'qux']];
yield 'correct keys, with failing rule' => [new KeySet(new Key('foo', Stub::fail(1))), ['foo' => 'bar']];
}
}

View file

@ -10,97 +10,75 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\TestCase;
use Respect\Validation\Test\RuleTestCase;
use stdClass;
#[Group('rule')]
#[CoversClass(Nullable::class)]
final class NullableTest extends TestCase
final class NullableTest extends RuleTestCase
{
#[Test]
public function shouldNotValidateRuleWhenInputIsNull(): void
public function itShouldUseStandardTemplateWhenItHasNameWhenInputIsOptional(): void
{
$rule = new Nullable(Stub::pass(1));
self::assertTrue($rule->validate(null));
$result = $rule->evaluate(null);
self::assertSame($rule, $result->rule);
self::assertSame(Nullable::TEMPLATE_STANDARD, $result->template);
}
#[Test]
#[DataProvider('providerForNotNullable')]
public function shouldValidateRuleWhenInputIsNotNullable(mixed $input): void
public function itShouldUseNamedTemplateWhenItHasNameWhenInputIsNullable(): void
{
$rule = new Nullable(Stub::pass(1));
$rule->setName('foo');
self::assertTrue($rule->validate($input));
$result = $rule->evaluate(null);
self::assertSame($rule, $result->rule);
self::assertSame(Nullable::TEMPLATE_NAMED, $result->template);
}
#[Test]
#[DoesNotPerformAssertions]
public function shouldNotAssertRuleWhenInputIsNull(): void
public function itShouldUseWrappedRuleToEvaluateWhenNotNullable(): void
{
$sut = new Nullable(Stub::daze());
$sut->assert(null);
$input = new stdClass();
$wrapped = Stub::pass(2);
$rule = new Nullable($wrapped);
self::assertEquals($wrapped->evaluate($input), $rule->evaluate($input));
}
#[Test]
#[DataProvider('providerForNotNullable')]
public function shouldAssertRuleWhenInputIsNotNullable(mixed $input): void
/** @return iterable<string, array{Nullable, mixed}> */
public static function providerForValidInput(): iterable
{
$rule = Stub::pass(1);
$sut = new Nullable($rule);
$sut->assert($input);
self::assertSame([$input], $rule->inputs);
yield 'null' => [new Nullable(Stub::daze()), null];
yield 'not null with passing rule' => [new Nullable(Stub::pass(1)), 42];
}
#[Test]
#[DoesNotPerformAssertions]
public function shouldNotCheckRuleWhenInputIsNull(): void
/** @return iterable<array{Nullable, mixed}> */
public static function providerForInvalidInput(): iterable
{
$rule = new Nullable(Stub::daze());
$rule->check(null);
}
#[Test]
#[DataProvider('providerForNotNullable')]
public function shouldCheckRuleWhenInputIsNotNullable(mixed $input): void
{
$rule = Stub::pass(1);
$sut = new Nullable($rule);
$sut->check($input);
self::assertSame([$input], $rule->inputs);
}
/**
* @return mixed[][]
*/
public static function providerForNotNullable(): array
{
return [
[''],
[1],
[[]],
[' '],
[0],
['0'],
[0],
['0.0'],
[false],
[['']],
[[' ']],
[[0]],
[['0']],
[[false]],
[[[''], [0]]],
[new stdClass()],
];
yield [new Nullable(Stub::fail(1)), ''];
yield [new Nullable(Stub::fail(1)), 1];
yield [new Nullable(Stub::fail(1)), []];
yield [new Nullable(Stub::fail(1)), ' '];
yield [new Nullable(Stub::fail(1)), 0];
yield [new Nullable(Stub::fail(1)), '0'];
yield [new Nullable(Stub::fail(1)), 0];
yield [new Nullable(Stub::fail(1)), '0.0'];
yield [new Nullable(Stub::fail(1)), false];
yield [new Nullable(Stub::fail(1)), ['']];
yield [new Nullable(Stub::fail(1)), [' ']];
yield [new Nullable(Stub::fail(1)), [0]];
yield [new Nullable(Stub::fail(1)), ['0']];
yield [new Nullable(Stub::fail(1)), [false]];
yield [new Nullable(Stub::fail(1)), [[''], [0]]];
yield [new Nullable(Stub::fail(1)), new stdClass()];
}
}

View file

@ -10,137 +10,75 @@ 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\Test\Rules\Stub;
use Respect\Validation\Test\RuleTestCase;
use stdClass;
#[Group('rule')]
#[CoversClass(Optional::class)]
final class OptionalTest extends TestCase
final class OptionalTest extends RuleTestCase
{
#[Test]
#[DataProvider('providerForOptional')]
public function shouldNotValidateRuleWhenInputIsOptional(mixed $input): void
public function itShouldUseStandardTemplateWhenItHasNameWhenInputIsOptional(): void
{
$validatable = $this->createMock(Validatable::class);
$validatable
->expects(self::never())
->method('validate');
$rule = new Optional(Stub::pass(1));
$rule = new Optional($validatable);
$result = $rule->evaluate('');
self::assertTrue($rule->validate($input));
self::assertSame($rule, $result->rule);
self::assertSame(Optional::TEMPLATE_STANDARD, $result->template);
}
#[Test]
#[DataProvider('providerForNotOptional')]
public function shouldValidateRuleWhenInputIsNotOptional(mixed $input): void
public function itShouldUseNamedTemplateWhenItHasNameWhenInputIsOptional(): void
{
$validatable = $this->createMock(Validatable::class);
$validatable
->expects(self::once())
->method('validate')
->with($input)
->willReturn(true);
$rule = new Optional(Stub::pass(1));
$rule->setName('foo');
$rule = new Optional($validatable);
$result = $rule->evaluate('');
self::assertTrue($rule->validate($input));
self::assertSame($rule, $result->rule);
self::assertSame(Optional::TEMPLATE_NAMED, $result->template);
}
#[Test]
public function shouldNotAssertRuleWhenInputIsOptional(): void
public function itShouldUseWrappedRuleToEvaluateWhenNotOptional(): void
{
$validatable = $this->createMock(Validatable::class);
$validatable
->expects(self::never())
->method('assert');
$input = new stdClass();
$rule = new Optional($validatable);
$wrapped = Stub::pass(2);
$rule = new Optional($wrapped);
$rule->assert('');
self::assertEquals($wrapped->evaluate($input), $rule->evaluate($input));
}
#[Test]
public function shouldAssertRuleWhenInputIsNotOptional(): void
/** @return iterable<string, array{Optional, mixed}> */
public static function providerForValidInput(): iterable
{
$input = 'foo';
$validatable = $this->createMock(Validatable::class);
$validatable
->expects(self::once())
->method('assert')
->with($input);
$rule = new Optional($validatable);
$rule->assert($input);
yield 'null' => [new Optional(Stub::daze()), null];
yield 'empty string' => [new Optional(Stub::daze()), ''];
yield 'not optional' => [new Optional(Stub::pass(1)), 42];
}
#[Test]
public function shouldNotCheckRuleWhenInputIsOptional(): void
/** @return iterable<array{Optional, mixed}> */
public static function providerForInvalidInput(): iterable
{
$validatable = $this->createMock(Validatable::class);
$validatable
->expects(self::never())
->method('check');
$rule = new Optional($validatable);
$rule->check('');
}
#[Test]
public function shouldCheckRuleWhenInputIsNotOptional(): void
{
$input = 'foo';
$validatable = $this->createMock(Validatable::class);
$validatable
->expects(self::once())
->method('check')
->with($input);
$rule = new Optional($validatable);
$rule->check($input);
}
/**
* @return mixed[][]
*/
public static function providerForOptional(): array
{
return [
[null],
[''],
];
}
/**
* @return mixed[][]
*/
public static function providerForNotOptional(): array
{
return [
[1],
[[]],
[' '],
[0],
['0'],
[0],
['0.0'],
[false],
[['']],
[[' ']],
[[0]],
[['0']],
[[false]],
[[[''], [0]]],
[new stdClass()],
];
yield [new Optional(Stub::fail(1)), 1];
yield [new Optional(Stub::fail(1)), []];
yield [new Optional(Stub::fail(1)), ' '];
yield [new Optional(Stub::fail(1)), 0];
yield [new Optional(Stub::fail(1)), '0'];
yield [new Optional(Stub::fail(1)), 0];
yield [new Optional(Stub::fail(1)), '0.0'];
yield [new Optional(Stub::fail(1)), false];
yield [new Optional(Stub::fail(1)), ['']];
yield [new Optional(Stub::fail(1)), [' ']];
yield [new Optional(Stub::fail(1)), [0]];
yield [new Optional(Stub::fail(1)), ['0']];
yield [new Optional(Stub::fail(1)), [false]];
yield [new Optional(Stub::fail(1)), [[''], [0]]];
yield [new Optional(Stub::fail(1)), new stdClass()];
}
}

View file

@ -12,37 +12,24 @@ namespace Respect\Validation\Rules;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\Rules\WrapperStub;
use Respect\Validation\Test\TestCase;
#[Group('core')]
#[CoversClass(AbstractWrapper::class)]
final class AbstractWrapperTest extends TestCase
#[CoversClass(Wrapper::class)]
final class WrapperTest extends TestCase
{
#[Test]
public function shouldUseWrappedToValidate(): void
public function shouldUseWrappedToEvaluate(): void
{
$sut = new WrapperStub(Stub::pass(1));
$wrapped = Stub::pass(2);
self::assertTrue($sut->validate('Whatever'));
}
$wrapper = new WrapperStub($wrapped);
#[Test]
public function shouldUseWrappedToAssert(): void
{
$sut = new WrapperStub(Stub::fail(1));
$this->expectException(ValidationException::class);
$sut->assert('Whatever');
}
$input = 'Whatever';
#[Test]
public function shouldUseWrappedToCheck(): void
{
$sut = new WrapperStub(Stub::fail(1));
$this->expectException(ValidationException::class);
$sut->check('Whatever');
self::assertEquals($wrapped->evaluate($input), $wrapper->evaluate($input));
}
#[Test]
@ -56,5 +43,20 @@ final class AbstractWrapperTest extends TestCase
$sut->setName($name);
self::assertSame($name, $rule->getName());
self::assertSame($name, $sut->getName());
}
#[Test]
public function shouldPassTemplateOnToWrapped(): void
{
$template = 'Whatever';
$rule = Stub::pass(1);
$sut = new WrapperStub($rule);
$sut->setTemplate($template);
self::assertSame($template, $rule->getTemplate());
self::assertSame($template, $sut->getTemplate());
}
}