Skip to content

Commit

Permalink
Improve KeySet rule
Browse files Browse the repository at this point in the history
After changes in the key-related rules, the KeySet rule became unusable.
Besides, when evaluating an input, it wasn't reporting every single
failure because it would not validate the items in the array if they had
missing or extra keys.

This commit will make several improvements to the rule. It will create
some not(keyExists($key)) rules for the extra keys, which makes the
error reporting much better. A limit of 10 additional keys will show up
when asserting an input with extra keys. I put that limit in place to
prevent the creation of too many rules.
  • Loading branch information
henriquemoody committed Dec 2, 2024
1 parent 61e9c0c commit 2aa5e39
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 174 deletions.
50 changes: 32 additions & 18 deletions docs/rules/KeySet.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,60 @@
# KeySet

- `KeySet(Key $rule, Key ...$rules)`
- `KeySet(KeyRelated $rule, KeyRelated ...$rules)`

Validates a keys in a defined structure.

```php
$dict = ['foo' => 42];
v::keySet(
v::keyExists('foo'),
v::keyExists('bar')
)->validate(['foo' => 'whatever', 'bar' => 'something']); // true
```

It will validate the keys in the array with the rules passed in the constructor.
```php
v::keySet(
v::key('foo', v::intVal())
)->validate($dict); // true
)->validate(['foo' => 42]); // true

v::keySet(
v::key('foo', v::intVal())
)->validate(['foo' => 'string']); // false
```

Extra keys are not allowed:
```php
$dict = ['foo' => 42, 'bar' => 'String'];

v::keySet(
v::key('foo', v::intVal())
)->validate($dict); // false
)->validate(['foo' => 42, 'bar' => 'String']); // false
```

Missing required keys are not allowed:
```php
$dict = ['foo' => 42, 'bar' => 'String'];

v::keySet(
v::key('foo', v::intVal()),
v::key('bar', v::stringType()),
v::key('baz', v::boolType())
)->validate($dict); // false
)->validate(['foo' => 42, 'bar' => 'String']); // false
```

Missing non-required keys are allowed:
```php
$dict = ['foo' => 42, 'bar' => 'String'];

v::keySet(
v::key('foo', v::intVal()),
v::key('bar', v::stringType()),
v::key('baz', v::boolType(), false)
)->validate($dict); // true
v::keyOptional('baz', v::boolType())
)->validate(['foo' => 42, 'bar' => 'String']); // true
```

Alternatively, you can pass a chain of key-related rules to `keySet()`:
```php
v::keySet(
v::create()
->key('foo', v::intVal())
->key('bar', v::stringType())
->keyOptional('baz', v::boolType())
)->validate(['foo' => 42, 'bar' => 'String']); // true
```

It is not possible to negate `keySet()` rules with `not()`.
Expand All @@ -55,11 +69,11 @@ The keys' order is not considered in the validation.

## Changelog

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
| Version | Description |
|--------:|-------------------------------------------------------|
| 3.0.0 | Requires at least one key-related rule |
| 2.3.0 | KeySet is NonNegatable, fixed message with extra keys |
| 1.0.0 | Created |

***
See also:
Expand Down
43 changes: 0 additions & 43 deletions library/Helpers/CanExtractRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,16 @@

namespace Respect\Validation\Helpers;

use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Rules\Core\Composite;
use Respect\Validation\Rules\Not;
use Respect\Validation\Validatable;
use Respect\Validation\Validator;
use Throwable;

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;
}

private function extractSiblingSuitableRule(Validatable $rule, Throwable $throwable): Validatable
{
$this->assertSingleRule($rule, $throwable);
Expand Down Expand Up @@ -73,26 +52,4 @@ private function assertSingleRule(Validatable $rule, Throwable $throwable): void
throw $throwable;
}
}

/**
* @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);
}
}
17 changes: 17 additions & 0 deletions library/Rules/Core/KeyRelated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/*
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
* SPDX-License-Identifier: MIT
*/

declare(strict_types=1);

namespace Respect\Validation\Rules\Core;

use Respect\Validation\Validatable;

interface KeyRelated extends Validatable
{
public function getKey(): int|string;
}
3 changes: 2 additions & 1 deletion library/Rules/Key.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@

use Respect\Validation\Helpers\CanBindEvaluateRule;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\KeyRelated;
use Respect\Validation\Rules\Core\Wrapper;
use Respect\Validation\Validatable;

final class Key extends Wrapper
final class Key extends Wrapper implements KeyRelated
{
use CanBindEvaluateRule;

Expand Down
8 changes: 7 additions & 1 deletion library/Rules/KeyExists.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use ArrayAccess;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\KeyRelated;
use Respect\Validation\Rules\Core\Standard;

use function array_key_exists;
Expand All @@ -21,13 +22,18 @@
'{{name}} must be present',
'{{name}} must not be present',
)]
final class KeyExists extends Standard
final class KeyExists extends Standard implements KeyRelated
{
public function __construct(
private readonly int|string $key
) {
}

public function getKey(): int|string
{
return $this->key;
}

public function evaluate(mixed $input): Result
{
return new Result($this->hasKey($input), $input, $this, name: (string) $this->key, id: (string) $this->key);
Expand Down
8 changes: 7 additions & 1 deletion library/Rules/KeyOptional.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@

use Respect\Validation\Helpers\CanBindEvaluateRule;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\KeyRelated;
use Respect\Validation\Rules\Core\Wrapper;
use Respect\Validation\Validatable;

final class KeyOptional extends Wrapper
final class KeyOptional extends Wrapper implements KeyRelated
{
use CanBindEvaluateRule;

Expand All @@ -26,6 +27,11 @@ public function __construct(
parent::__construct($rule);
}

public function getKey(): int|string
{
return $this->key;
}

public function evaluate(mixed $input): Result
{
$keyExistsResult = $this->bindEvaluate(new KeyExists($this->key), $this, $input);
Expand Down
Loading

0 comments on commit 2aa5e39

Please sign in to comment.