Split "Key" rules

Currently, the Key rule has a third parameter that allows the validation
of the wrapped rule to be optional, meaning that the validation will
only happen if the key exists. That parameter makes the rule harder to
understand at times.

I'm splitting the Key rule into Key, KeyExists, and KeyOptional. That
way, it becomes apparent when someone wants only to validate whether a
key exists or if they're going to validate the value of the key only
when it exists.

I deliberately didn't create an abstract class because those rules are
different enough not to have an abstraction. In fact, I can see myself
deleting the  "AbstractRelated" in the upcoming changes.

With these changes, the KeySet rule will not accept validating if the
key exists or validating the value only if the key exists. I should
refactor that soon, and I will likely need to create a common interface
for Key, KeyExists, and KeyOptional.

Signed-off-by: Henrique Moody <henriquemoody@gmail.com>
This commit is contained in:
Henrique Moody 2024-02-22 23:05:45 +01:00
parent c946f16f60
commit a647a4737b
No known key found for this signature in database
GPG key ID: 221E9281655813A6
27 changed files with 845 additions and 225 deletions

View file

@ -10,7 +10,9 @@
- [EndsWith](rules/EndsWith.md)
- [In](rules/In.md)
- [Key](rules/Key.md)
- [KeyExists](rules/KeyExists.md)
- [KeyNested](rules/KeyNested.md)
- [KeyOptional](rules/KeyOptional.md)
- [KeySet](rules/KeySet.md)
- [Sorted](rules/Sorted.md)
- [StartsWith](rules/StartsWith.md)
@ -242,7 +244,9 @@
## Structures
- [Key](rules/Key.md)
- [KeyExists](rules/KeyExists.md)
- [KeyNested](rules/KeyNested.md)
- [KeyOptional](rules/KeyOptional.md)
- [KeySet](rules/KeySet.md)
- [Property](rules/Property.md)
@ -349,7 +353,9 @@
- [IterableVal](rules/IterableVal.md)
- [Json](rules/Json.md)
- [Key](rules/Key.md)
- [KeyExists](rules/KeyExists.md)
- [KeyNested](rules/KeyNested.md)
- [KeyOptional](rules/KeyOptional.md)
- [KeySet](rules/KeySet.md)
- [LanguageCode](rules/LanguageCode.md)
- [LazyConsecutive](rules/LazyConsecutive.md)

View file

@ -32,6 +32,8 @@ See also:
- [IntType](IntType.md)
- [IterableType](IterableType.md)
- [IterableVal](IterableVal.md)
- [KeyExists](KeyExists.md)
- [KeyOptional](KeyOptional.md)
- [NullType](NullType.md)
- [ObjectType](ObjectType.md)
- [ResourceType](ResourceType.md)

View file

@ -35,6 +35,8 @@ See also:
- [IterableType](IterableType.md)
- [IterableVal](IterableVal.md)
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [KeyOptional](KeyOptional.md)
- [KeySet](KeySet.md)
- [ScalarVal](ScalarVal.md)
- [Sorted](Sorted.md)

View file

@ -33,7 +33,8 @@ Using `v::call()` you can do this in a single chain:
```php
v::call(
'parse_url',
v::arrayVal()->key('scheme', v::startsWith('http'))
v::arrayVal()
->key('scheme', v::startsWith('http'))
->key('host', v::domain())
->key('path', v::stringType())
->key('query', v::notEmpty())

View file

@ -47,6 +47,8 @@ See also:
- [IterableType](IterableType.md)
- [IterableVal](IterableVal.md)
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [KeyOptional](KeyOptional.md)
- [Min](Min.md)
- [NotEmpty](NotEmpty.md)
- [Unique](Unique.md)

View file

@ -1,33 +1,32 @@
# Key
- `Key(mixed $key)`
- `Key(mixed $key, Validatable $rule)`
- `Key(mixed $key, Validatable $rule, bool $mandatory)`
- `Key(int|string $key, Validatable $rule)`
Validates an array key.
Validates the value of an array against a given rule.
```php
$dict = [
'foo' => 'bar'
];
v::key('name', v::stringType())->validate(['name' => 'The Respect Panda']); // true
v::key('foo')->validate($dict); // true
```
v::key('email', v::email())->validate(['email' => 'therespectpanda@gmail.com']); // true
You can also validate the key value itself:
```php
v::key('foo', v::equals('bar'))->validate($dict); // true
```
Third parameter makes the key presence optional:
```php
v::key('lorem', v::stringType(), false)->validate($dict); // true
v::key('age', v::intVal())->validate([]); // false
```
The name of this validator is automatically set to the key name.
```php
v::key('email', v::email())->assert([]);
// message: email must be present
v::key('email', v::email())->assert(['email' => 'not email']);
// message: email must be valid email
```
## Note
* To validate if a key exists, use [KeyExists](KeyExists.md) instead.
* To validate an array against a given rule if the key exists, use [KeyOptional](KeyOptional.md) instead.
## Categorization
- Arrays
@ -36,15 +35,18 @@ The name of this validator is automatically set to the key name.
## Changelog
Version | Description
--------|-------------
0.3.9 | Created
| Version | Description |
|--------:|----------------------------------------------------------------------|
| 3.0.0 | Split by [KeyExists](KeyExists.md) and [KeyOptional](KeyOptional.md) |
| 0.3.9 | Created |
***
See also:
- [ArrayVal](ArrayVal.md)
- [Each](Each.md)
- [KeyExists](KeyExists.md)
- [KeyNested](KeyNested.md)
- [KeyOptional](KeyOptional.md)
- [KeySet](KeySet.md)
- [Property](Property.md)

43
docs/rules/KeyExists.md Normal file
View file

@ -0,0 +1,43 @@
# KeyExists
- `KeyExists(int|string $key)`
Validates if the given key exists in an array.
```php
v::keyExists('name')->validate(['name' => 'The Respect Panda']); // true
v::keyExists('name')->validate(['email' => 'therespectpanda@gmail.com']); // false
v::keyExists(0)->validate(['a', 'b', 'c']); // true
v::keyExists(4)->validate(['a', 'b', 'c']); // false
v::keyExists('username')->validate(new ArrayObject(['username' => 'therespectpanda'])); // true
v::keyExists(5)->validate(new ArrayObject(['a', 'b', 'c'])); // false
```
## Notes
* To validate an array against a given rule if the key exists, use [KeyOptional](KeyOptional.md) instead.
* To validate an array against a given rule requiring the key to exist, use [Key](Key.md) instead.
## Categorization
- Arrays
- Structures
## Changelog
| Version | Description |
| ------: |----------------------------|
| 3.0.0 | Created from [Key](Key.md) |
***
See also:
- [ArrayType](ArrayType.md)
- [ArrayVal](ArrayVal.md)
- [Each](Each.md)
- [Key](Key.md)
- [KeyOptional](KeyOptional.md)
- [Property](Property.md)

60
docs/rules/KeyOptional.md Normal file
View file

@ -0,0 +1,60 @@
# KeyOptional
- `KeyOptional(int|string $key, Validatable $rule)`
Validates the value of an array against a given rule when the key exists.
```php
v::keyOptional('name', v::stringType())->validate([]); // true
v::keyOptional('name', v::stringType())->validate(['name' => 'The Respect Panda']); // true
v::keyOptional('email', v::email())->validate([]); // true
v::keyOptional('email', v::email())->validate(['email' => 'therespectpanda@gmail.com']); // true
v::keyOptional('age', v::intVal())->validate(['age' => 'Twenty-Five']); // false
```
The name of this validator is automatically set to the key name.
```php
v::keyOptional('age', v::intVal())->assert(['age' => 'Twenty-Five']);
// message: age must be an integer number
```
## Note
This rule will pass for anything that is not an array because it will always pass when it doesn't find a key. If you
want to ensure the input is an array, use [ArrayType](ArrayType.md) with it.
```php
v::arrayType()->keyOptional('phone', v::phone())->assert('This is not an array');
// message: "This is not an array" must be of type array
```
Below are some other rules that are tightly related to `KeyOptional`:
* To validate if a key exists, use [KeyExists](KeyExists.md) instead.
* To validate an array against a given rule requiring the key to exist, use [Key](Key.md) instead.
## Categorization
- Arrays
- Structures
## Changelog
| Version | Description |
| ------: |----------------------------|
| 3.0.0 | Created from [Key](Key.md) |
***
See also:
- [ArrayType](ArrayType.md)
- [ArrayVal](ArrayVal.md)
- [Each](Each.md)
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [Property](Property.md)
[array]: https://www.php.net/array
[ArrayAccess]: https://www.php.net/arrayaccess

View file

@ -44,5 +44,7 @@ Version | Description
See also:
- [Key](Key.md)
- [KeyExists](KeyExists.md)
- [KeyNested](KeyNested.md)
- [KeyOptional](KeyOptional.md)
- [ObjectType](ObjectType.md)

View file

@ -164,11 +164,11 @@ interface ChainedValidator extends Validatable
public function json(): ChainedValidator;
public function key(
string $reference,
?Validatable $referenceValidator = null,
bool $mandatory = true
): ChainedValidator;
public function key(int|string $key, Validatable $rule): ChainedValidator;
public function keyExists(int|string $key): ChainedValidator;
public function keyOptional(int|string $key, Validatable $rule): ChainedValidator;
public function keyNested(
string $reference,

View file

@ -166,11 +166,11 @@ interface StaticValidator
public static function json(): ChainedValidator;
public static function key(
string $reference,
?Validatable $referenceValidator = null,
bool $mandatory = true
): ChainedValidator;
public static function key(int|string $key, Validatable $rule): ChainedValidator;
public static function keyExists(int|string $key): ChainedValidator;
public static function keyOptional(int|string $key, Validatable $rule): ChainedValidator;
public static function keyNested(
string $reference,

View file

@ -9,45 +9,39 @@ 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\Message\Template;
use Respect\Validation\Helpers\CanBindEvaluateRule;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\Wrapper;
use Respect\Validation\Validatable;
use function array_key_exists;
use function is_array;
use function is_scalar;
#[ExceptionClass(NonOmissibleValidationException::class)]
#[Template(
'{{name}} must be present',
'{{name}} must not be present',
self::TEMPLATE_NOT_PRESENT,
)]
#[Template(
'{{name}} must be valid',
'{{name}} must not be valid',
self::TEMPLATE_INVALID,
)]
final class Key extends AbstractRelated
final class Key extends Wrapper
{
public function __construct(mixed $reference, ?Validatable $rule = null, bool $mandatory = true)
use CanBindEvaluateRule;
public function __construct(
private readonly int|string $key,
Validatable $rule,
) {
$rule->setName($rule->getName() ?? (string) $key);
parent::__construct($rule);
}
public function getKey(): int|string
{
if (!is_scalar($reference) || $reference === '') {
throw new ComponentException('Invalid array key name');
return $this->key;
}
public function evaluate(mixed $input): Result
{
$keyExistsResult = $this->bindEvaluate(new KeyExists($this->key), $this, $input);
if (!$keyExistsResult->isValid) {
return $keyExistsResult;
}
parent::__construct($reference, $rule, $mandatory);
}
$child = $this->rule->evaluate($input[$this->key]);
public function getReferenceValue(mixed $input): mixed
{
return $input[$this->getReference()];
}
public function hasReference(mixed $input): bool
{
return is_array($input) && array_key_exists($this->getReference(), $input);
return (new Result($child->isValid, $input, $this))
->withChildren($child)
->withNameIfMissing($this->rule->getName() ?? (string) $this->key);
}
}

View file

@ -0,0 +1,39 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\Standard;
use function array_key_exists;
use function is_array;
#[Template(
'{{name}} must be present',
'{{name}} must not be present',
)]
final class KeyExists extends Standard
{
public function __construct(
private readonly int|string $key
) {
}
public function evaluate(mixed $input): Result
{
return new Result($this->hasKey($input), $input, $this, name: (string) $this->key);
}
private function hasKey(mixed $input): bool
{
return is_array($input) && array_key_exists($this->key, $input);
}
}

View file

@ -0,0 +1,42 @@
<?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\CanBindEvaluateRule;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\Wrapper;
use Respect\Validation\Validatable;
final class KeyOptional extends Wrapper
{
use CanBindEvaluateRule;
public function __construct(
private readonly int|string $key,
Validatable $rule,
) {
$rule->setName($rule->getName() ?? (string) $key);
parent::__construct($rule);
}
public function evaluate(mixed $input): Result
{
$keyExistsResult = $this->bindEvaluate(new KeyExists($this->key), $this, $input);
if (!$keyExistsResult->isValid) {
return $keyExistsResult->withInvertedMode();
}
$child = $this->rule->evaluate($input[$this->key]);
return (new Result($child->isValid, $input, $this))
->withChildren($child)
->withNameIfMissing($this->rule->getName() ?? (string) $this->key);
}
}

View file

@ -49,7 +49,7 @@ final class KeySet extends Wrapper
/** @var array<Key> $keyRules */
$keyRules = $this->extractMany(array_merge([$rule], $rules), Key::class);
$this->keys = array_map(static fn(Key $keyRule) => $keyRule->getReference(), $keyRules);
$this->keys = array_map(static fn(Key $keyRule) => $keyRule->getKey(), $keyRules);
parent::__construct(count($keyRules) === 1 ? $keyRules[0] : new AllOf(...$keyRules));
}

View file

@ -12,20 +12,18 @@ exceptionFullMessage(static function (): void {
->key(
'mysql',
v::create()
->key('host', v::stringType(), true)
->key('user', v::stringType(), true)
->key('password', v::stringType(), true)
->key('schema', v::stringType(), true),
true
->key('host', v::stringType())
->key('user', v::stringType())
->key('password', v::stringType())
->key('schema', v::stringType())
)
->key(
'postgresql',
v::create()
->key('host', v::stringType(), true)
->key('user', v::stringType(), true)
->key('password', v::stringType(), true)
->key('schema', v::stringType(), true),
true
->key('host', v::stringType())
->key('user', v::stringType())
->key('password', v::stringType())
->key('schema', v::stringType())
)
->setName('the given data')
->assert([

View file

@ -12,20 +12,18 @@ exceptionMessages(static function (): void {
->key(
'mysql',
v::create()
->key('host', v::stringType(), true)
->key('user', v::stringType(), true)
->key('password', v::stringType(), true)
->key('schema', v::stringType(), true),
true
->key('host', v::stringType())
->key('user', v::stringType())
->key('password', v::stringType())
->key('schema', v::stringType())
)
->key(
'postgresql',
v::create()
->key('host', v::stringType(), true)
->key('user', v::stringType(), true)
->key('password', v::stringType(), true)
->key('schema', v::stringType(), true),
true
->key('host', v::stringType())
->key('user', v::stringType())
->key('password', v::stringType())
->key('schema', v::stringType())
)
->assert([
'mysql' => [

View file

@ -13,20 +13,18 @@ exceptionMessages(
->key(
'mysql',
v::create()
->key('host', v::stringType(), true)
->key('user', v::stringType(), true)
->key('password', v::stringType(), true)
->key('schema', v::stringType(), true),
true
->key('host', v::stringType())
->key('user', v::stringType())
->key('password', v::stringType())
->key('schema', v::stringType())
)
->key(
'postgresql',
v::create()
->key('host', v::stringType(), true)
->key('user', v::stringType(), true)
->key('password', v::stringType(), true)
->key('schema', v::stringType(), true),
true
->key('host', v::stringType())
->key('user', v::stringType())
->key('password', v::stringType())
->key('schema', v::stringType())
)
->setTemplates([
'mysql' => [

View file

@ -12,20 +12,18 @@ exceptionFullMessage(static function (): void {
->key(
'mysql',
v::create()
->key('host', v::stringType(), true)
->key('user', v::stringType(), true)
->key('password', v::stringType(), true)
->key('schema', v::stringType(), true),
true
->key('host', v::stringType())
->key('user', v::stringType())
->key('password', v::stringType())
->key('schema', v::stringType())
)
->key(
'postgresql',
v::create()
->key('host', v::stringType(), true)
->key('user', v::stringType(), true)
->key('password', v::stringType(), true)
->key('schema', v::stringType(), true),
true
->key('host', v::stringType())
->key('user', v::stringType())
->key('password', v::stringType())
->key('schema', v::stringType())
)
->setName('the given data')
->assert([

View file

@ -0,0 +1,159 @@
--FILE--
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use Respect\Validation\Validator as v;
run([
// Simple
'Missing key' => [v::key('foo', v::intType()), []],
'Default' => [v::key('foo', v::intType()), ['foo' => 'string']],
'Negative' => [v::not(v::key('foo', v::intType())), ['foo' => 12]],
'Double-negative with missing key' => [
v::not(v::not(v::key('foo', v::intType()))),
[],
],
// With custom name
'With wrapped name, missing key' => [
v::key('foo', v::intType()->setName('Wrapped'))->setName('Wrapper'),
[],
],
'With wrapped name, default' => [
v::key('foo', v::intType()->setName('Wrapped'))->setName('Wrapper'),
['foo' => 'string'],
],
'With wrapped name, negative' => [
v::not(v::key('foo', v::intType()->setName('Wrapped'))->setName('Wrapper'))->setName('Not'),
['foo' => 12],
],
'With wrapper name, default' => [
v::key('foo', v::intType())->setName('Wrapper'),
['foo' => 'string'],
],
'With wrapper name, missing key' => [
v::key('foo', v::intType())->setName('Wrapper'),
[],
],
'With wrapper name, negative' => [
v::not(v::key('foo', v::intType())->setName('Wrapper'))->setName('Not'),
['foo' => 12],
],
'With "Not" name, negative' => [
v::not(v::key('foo', v::intType()))->setName('Not'),
['foo' => 12],
],
// With custom template
'With template, default' => [v::key('foo', v::intType()), ['foo' => 'string'], 'That key is off-key'],
'With template, negative' => [v::not(v::key('foo', v::intType())), ['foo' => 12], 'No off-key key'],
]);
?>
--EXPECT--
Missing key
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be present
- foo must be present
[
'foo' => 'foo must be present',
]
Default
⎺⎺⎺⎺⎺⎺⎺
foo must be of type integer
- foo must be of type integer
[
'foo' => 'foo must be of type integer',
]
Negative
⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be of type integer
- foo must not be of type integer
[
'foo' => 'foo must not be of type integer',
]
Double-negative with missing key
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be present
- foo must be present
[
'foo' => 'foo must be present',
]
With wrapped name, missing key
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must be present
- Wrapped must be present
[
'Wrapped' => 'Wrapped must be present',
]
With wrapped name, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must be of type integer
- Wrapped must be of type integer
[
'Wrapped' => 'Wrapped must be of type integer',
]
With wrapped name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must not be of type integer
- Wrapped must not be of type integer
[
'Wrapped' => 'Wrapped must not be of type integer',
]
With wrapper name, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be of type integer
- foo must be of type integer
[
'foo' => 'foo must be of type integer',
]
With wrapper name, missing key
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be present
- foo must be present
[
'foo' => 'foo must be present',
]
With wrapper name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be of type integer
- foo must not be of type integer
[
'foo' => 'foo must not be of type integer',
]
With "Not" name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be of type integer
- foo must not be of type integer
[
'foo' => 'foo must not be of type integer',
]
With template, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
That key is off-key
- That key is off-key
[
'foo' => 'That key is off-key',
]
With template, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
No off-key key
- No off-key key
[
'foo' => 'No off-key key',
]

View file

@ -0,0 +1,49 @@
--FILE--
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use Respect\Validation\Validator as v;
run([
'Default mode' => [v::keyExists('foo'), ['bar' => 'baz']],
'Negative mode' => [v::not(v::keyExists('foo')), ['foo' => 'baz']],
'Custom name' => [v::keyExists('foo')->setName('Custom name'), ['bar' => 'baz']],
'Custom template' => [v::keyExists('foo'), ['bar' => 'baz'], 'Custom template for `{{name}}`'],
]);
?>
--EXPECT--
Default mode
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be present
- foo must be present
[
'foo' => 'foo must be present',
]
Negative mode
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be present
- foo must not be present
[
'foo' => 'foo must not be present',
]
Custom name
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Custom name must be present
- Custom name must be present
[
'Custom name' => 'Custom name must be present',
]
Custom template
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Custom template for `foo`
- Custom template for `foo`
[
'foo' => 'Custom template for `foo`',
]

View file

@ -0,0 +1,126 @@
--FILE--
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use Respect\Validation\Validator as v;
run([
// Simple
'Default' => [v::keyOptional('foo', v::intType()), ['foo' => 'string']],
'Negative' => [v::not(v::keyOptional('foo', v::intType())), ['foo' => 12]],
'Negative with missing key' => [
v::not(v::keyOptional('foo', v::intType())),
[],
],
// With custom name
'With wrapped name, default' => [
v::keyOptional('foo', v::intType()->setName('Wrapped'))->setName('Wrapper'),
['foo' => 'string'],
],
'With wrapped name, negative' => [
v::not(v::keyOptional('foo', v::intType()->setName('Wrapped'))->setName('Wrapper'))->setName('Not'),
['foo' => 12],
],
'With wrapper name, default' => [
v::keyOptional('foo', v::intType())->setName('Wrapper'),
['foo' => 'string'],
],
'With wrapper name, negative' => [
v::not(v::keyOptional('foo', v::intType())->setName('Wrapper'))->setName('Not'),
['foo' => 12],
],
'With "Not" name, negative' => [
v::not(v::keyOptional('foo', v::intType()))->setName('Not'),
['foo' => 12],
],
// With custom template
'With template, default' => [v::keyOptional('foo', v::intType()), ['foo' => 'string'], 'That key is off-key'],
'With template, negative' => [v::not(v::keyOptional('foo', v::intType())), ['foo' => 12], 'No off-key key'],
]);
?>
--EXPECT--
Default
⎺⎺⎺⎺⎺⎺⎺
foo must be of type integer
- foo must be of type integer
[
'foo' => 'foo must be of type integer',
]
Negative
⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be of type integer
- foo must not be of type integer
[
'foo' => 'foo must not be of type integer',
]
Negative with missing key
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be present
- foo must be present
[
'foo' => 'foo must be present',
]
With wrapped name, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must be of type integer
- Wrapped must be of type integer
[
'Wrapped' => 'Wrapped must be of type integer',
]
With wrapped name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must not be of type integer
- Wrapped must not be of type integer
[
'Wrapped' => 'Wrapped must not be of type integer',
]
With wrapper name, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must be of type integer
- foo must be of type integer
[
'foo' => 'foo must be of type integer',
]
With wrapper name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be of type integer
- foo must not be of type integer
[
'foo' => 'foo must not be of type integer',
]
With "Not" name, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
foo must not be of type integer
- foo must not be of type integer
[
'foo' => 'foo must not be of type integer',
]
With template, default
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
That key is off-key
- That key is off-key
[
'foo' => 'That key is off-key',
]
With template, negative
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
No off-key key
- No off-key key
[
'foo' => 'No off-key key',
]

View file

@ -218,4 +218,45 @@ abstract class TestCase extends PHPUnitTestCase
'positive float' => [32.890],
];
}
/** @return array<array{mixed}> */
public static function providerForNonArrayValues(): array
{
$scalarValues = self::providerForNonScalarValues();
unset($scalarValues['array']);
return array_merge(
self::providerForIntegerValues(),
self::providerForBooleanValues(),
self::providerForFloatValues(),
self::providerForStringValues(),
$scalarValues,
);
}
/** @return array<string, array{string|int, array<mixed>}> */
public static function providerForArrayWithMissingKeys(): array
{
return [
'integer key, non-empty input' => [0, [1 => true, 2 => true]],
'string key, non-empty input' => ['foo', ['bar' => true, 'baz' => true]],
'integer key, empty input' => [0, []],
'string key, empty input' => ['foo', []],
];
}
/** @return array<string, array{string|int, array<mixed>}> */
public static function providerForArrayWithExistingKeys(): array
{
return [
'integer key with a single value array' => [1, [1 => true]],
'integer key with a multiple value array' => [2, [1 => true, 2 => true]],
'string key with a single value array' => ['foo', ['foo' => true, 'bar' => true]],
'string key with a multiple value array' => ['bar', ['foo' => true, 'bar' => true]],
'integer key with null for a value' => [0, [null]],
'string key with null for a value' => ['foo', ['foo' => null]],
'integer key with false for a value' => [0, [false]],
'string key with false for a value' => ['foo', ['foo' => false]],
];
}
}

View file

@ -0,0 +1,48 @@
<?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\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\TestCase;
#[CoversClass(KeyExists::class)]
final class KeyExistsTest extends TestCase
{
#[Test]
#[DataProvider('providerForNonArrayValues')]
public function itShouldAlwaysInvalidateNonArrayValues(mixed $input): void
{
$rule = new KeyExists(0);
self::assertInvalidInput($rule, $input);
}
/** @param array<mixed> $input */
#[Test]
#[DataProvider('providerForArrayWithMissingKeys')]
public function itShouldInvalidateMissingKeys(int|string $key, array $input): void
{
$rule = new KeyExists($key);
self::assertInvalidInput($rule, $input);
}
/** @param array<mixed> $input */
#[Test]
#[DataProvider('providerForArrayWithExistingKeys')]
public function itShouldValidateExistingKeys(int|string $key, array $input): void
{
$rule = new KeyExists($key);
self::assertValidInput($rule, $input);
}
}

View file

@ -0,0 +1,78 @@
<?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\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\TestCase;
#[Group('rule')]
#[CoversClass(KeyOptional::class)]
final class KeyOptionalTest extends TestCase
{
#[Test]
#[DataProvider('providerForNonArrayValues')]
public function itShouldAlwaysValidateNonArrayValues(mixed $input): void
{
$rule = new KeyOptional(0, Stub::daze());
self::assertValidInput($rule, $input);
}
/** @param array<mixed> $input */
#[Test]
#[DataProvider('providerForArrayWithMissingKeys')]
public function itShouldAlwaysValidateMissingKeys(int|string $key, array $input): void
{
$rule = new KeyOptional($key, Stub::daze());
self::assertValidInput($rule, $input);
}
/** @param array<mixed> $input */
#[Test]
#[DataProvider('providerForArrayWithExistingKeys')]
public function itShouldValidateExistingKeysWithWrappedRule(int|string $key, array $input): void
{
$wrapped = Stub::pass(1);
$rule = new KeyOptional($key, $wrapped);
$rule->evaluate($input);
self::assertEquals($wrapped->inputs, [$input[$key]]);
}
#[Test]
public function itShouldUpdateWrappedNameWithTheGivenKeyWhenItIsString(): void
{
$key = 'toodaloo';
$wrapped = Stub::daze();
new KeyOptional($key, $wrapped);
self::assertEquals($key, $wrapped->getName());
}
#[Test]
public function itShouldUpdateWrappedNameWithTheGivenKeyWhenItIsInteger(): void
{
$key = 123;
$wrapped = Stub::daze();
new KeyOptional($key, $wrapped);
self::assertEquals($key, $wrapped->getName());
}
}

View file

@ -21,16 +21,15 @@ final class KeySetTest extends RuleTestCase
/** @return iterable<string, array{KeySet, mixed}> */
public static function providerForValidInput(): iterable
{
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']];
}
/** @return iterable<string, array{KeySet, mixed}> */
public static function providerForInvalidInput(): iterable
{
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 'not array' => [new KeySet(new Key('foo', Stub::daze())), null];
yield 'missing keys' => [new KeySet(new Key('foo', Stub::daze())), []];
yield 'extra keys' => [new KeySet(new Key('foo', Stub::daze())), ['foo' => 'bar', 'baz' => 'qux']];
yield 'correct keys, with failing rule' => [new KeySet(new Key('foo', Stub::fail(1))), ['foo' => 'bar']];
}
}

View file

@ -10,145 +10,78 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;
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 Throwable;
#[Group('rule')]
#[CoversClass(AbstractRelated::class)]
#[CoversClass(Key::class)]
final class KeyTest extends TestCase
{
#[Test]
public function arrayWithPresentKeyShouldReturnTrue(): void
#[DataProvider('providerForNonArrayValues')]
public function itShouldAlwaysInvalidateNonArrayValues(mixed $input): void
{
$validator = new Key('bar');
$someArray = [];
$someArray['bar'] = 'foo';
self::assertTrue($validator->validate($someArray));
$rule = new Key(0, Stub::daze());
self::assertInvalidInput($rule, $input);
}
/** @param array<mixed> $input */
#[Test]
#[DataProvider('providerForArrayWithMissingKeys')]
public function itShouldInvalidateMissingKeys(int|string $key, array $input): void
{
$rule = new Key($key, Stub::daze());
self::assertInvalidInput($rule, $input);
}
/** @param array<mixed> $input */
#[Test]
#[DataProvider('providerForArrayWithExistingKeys')]
public function itShouldValidateExistingKeysWithWrappedRule(int|string $key, array $input): void
{
$wrapped = Stub::pass(1);
$rule = new Key($key, $wrapped);
$rule->evaluate($input);
self::assertEquals($wrapped->inputs, [$input[$key]]);
}
#[Test]
public function arrayWithNumericKeyShouldReturnTrue(): void
public function itShouldReturnDefinedKey(): void
{
$validator = new Key(0);
$someArray = [];
$someArray[0] = 'foo';
self::assertTrue($validator->validate($someArray));
$key = 'toodaloo';
$rule = new Key($key, Stub::daze());
self::assertSame($key, $rule->getKey());
}
#[Test]
public function emptyInputMustReturnFalse(): void
public function itShouldUpdateWrappedNameWithTheGivenKeyWhenItIsString(): void
{
$validator = new Key('someEmptyKey');
$input = '';
$key = 'toodaloo';
self::assertFalse($validator->validate($input));
$wrapped = Stub::daze();
new Key($key, $wrapped);
self::assertEquals($key, $wrapped->getName());
}
#[Test]
public function emptyInputMustNotAssert(): void
public function itShouldUpdateWrappedNameWithTheGivenKeyWhenItIsInteger(): void
{
$validator = new Key('someEmptyKey');
$key = 123;
$this->expectException(ValidationException::class);
$wrapped = Stub::daze();
$validator->assert('');
}
new Key($key, $wrapped);
#[Test]
public function emptyInputMustNotCheck(): void
{
$validator = new Key('someEmptyKey');
$this->expectException(ValidationException::class);
$validator->check('');
}
#[Test]
public function arrayWithEmptyKeyShouldReturnTrue(): void
{
$validator = new Key('someEmptyKey');
$input = [];
$input['someEmptyKey'] = '';
self::assertTrue($validator->validate($input));
}
#[Test]
public function shouldHaveTheSameReturnValueForAllValidators(): void
{
$rule = new Key('key', new NotEmpty());
$input = ['key' => ''];
try {
$rule->assert($input);
self::fail('`assert()` must throws exception');
} catch (Throwable $e) {
}
try {
$rule->check($input);
self::fail('`check()` must throws exception');
} catch (Throwable $e) {
}
self::assertFalse($rule->validate($input));
}
#[Test]
public function arrayWithAbsentKeyShouldThrowValidationException(): void
{
$validator = new Key('bar');
$someArray = [];
$someArray['baraaaaaa'] = 'foo';
$this->expectException(ValidationException::class);
$validator->assert($someArray);
}
#[Test]
public function notArrayShouldThrowValidationException(): void
{
$validator = new Key('bar');
$someArray = 123;
$this->expectException(ValidationException::class);
$validator->assert($someArray);
}
#[Test]
public function invalidConstructorParametersShouldThrowComponentExceptionUponInstantiation(): void
{
$this->expectException(ComponentException::class);
new Key(['invalid']);
}
#[Test]
#[DoesNotPerformAssertions]
public function extraValidatorShouldValidateKey(): void
{
$subValidator = new Length(1, 3);
$validator = new Key('bar', $subValidator);
$someArray = [];
$someArray['bar'] = 'foo';
$validator->assert($someArray);
}
#[Test]
public function notMandatoryExtraValidatorShouldPassWithAbsentKey(): void
{
$subValidator = new Length(1, 3);
$validator = new Key('bar', $subValidator, false);
$someArray = [];
self::assertTrue($validator->validate($someArray));
self::assertEquals($key, $wrapped->getName());
}
}