Skip to content

Commit

Permalink
Update NullOr to generate results with siblings
Browse files Browse the repository at this point in the history
Since I updated the validation engine[1], it became possible to create
results with siblings. This commit changes the "NullOr", allowing it to
create a result with a sibling when possible. That will improve the
clarity of the error message.

I also updated the documentation, since it was still called "Nullable"

[1]: 238f2d5
  • Loading branch information
henriquemoody committed Dec 4, 2024
1 parent 061a3c9 commit d1e0c8b
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 105 deletions.
4 changes: 2 additions & 2 deletions docs/08-list-of-rules-by-category.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@
- [Lazy](rules/Lazy.md)
- [NoneOf](rules/NoneOf.md)
- [Not](rules/Not.md)
- [Nullable](rules/Nullable.md)
- [NullOr](rules/NullOr.md)
- [OneOf](rules/OneOf.md)
- [Property](rules/Property.md)
- [PropertyOptional](rules/PropertyOptional.md)
Expand Down Expand Up @@ -393,8 +393,8 @@
- [NotEmoji](rules/NotEmoji.md)
- [NotEmpty](rules/NotEmpty.md)
- [NotUndef](rules/NotUndef.md)
- [NullOr](rules/NullOr.md)
- [NullType](rules/NullType.md)
- [Nullable](rules/Nullable.md)
- [Number](rules/Number.md)
- [NumericVal](rules/NumericVal.md)
- [ObjectType](rules/ObjectType.md)
Expand Down
62 changes: 62 additions & 0 deletions docs/rules/NullOr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# NullOr

- `NullOr(Validatable $rule)`

Validates the input using a defined rule when the input is not `null`.

## Usage

```php
v::nullable(v::email())->isValid(null); // true
v::nullable(v::email())->isValid('[email protected]'); // true
v::nullable(v::email())->isValid('not an email'); // false
```

## Prefix

For convenience, you can use `nullOr` as a prefix to any rule:

```php
v::nullOrEmail()->isValid('not an email'); // false
v::nullOrBetween(1, 3)->isValid(2); // true
v::nullOrBetween(1, 3)->isValid(null); // true
```

## Templates

| Id | Default | Inverted |
|-----------------------------|-----------------|----------------------|
| `NullOr::TEMPLATE_STANDARD` | or must be null | and must not be null |

The templates from this rule serve as message suffixes:

```php
v::nullOr(v::alpha())->assert('has1number');
// "has1number" must contain only letters (a-z) or must be null

v::not(v::nullOr(v::alpha()))->assert("alpha");
// "alpha" must not contain letters (a-z) and must not be null
```

## Template placeholders

| Placeholder | Description |
|-------------|------------------------------------------------------------------|
| `name` | The validated input or the custom validator name (if specified). |

## Categorization

- Nesting

## Changelog

| Version | Description |
|--------:|-----------------------|
| 3.0.0 | Renamed to `NullOr` |
| 2.0.0 | Created as `Nullable` |

***
See also:

- [NullType](NullType.md)
- [UndefOr](UndefOr.md)
2 changes: 1 addition & 1 deletion docs/rules/NullType.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ See also:
- [NotBlank](NotBlank.md)
- [NotEmpty](NotEmpty.md)
- [NotUndef](NotUndef.md)
- [Nullable](Nullable.md)
- [NullOr](NullOr.md)
- [Number](Number.md)
- [ObjectType](ObjectType.md)
- [ResourceType](ResourceType.md)
Expand Down
27 changes: 0 additions & 27 deletions docs/rules/Nullable.md

This file was deleted.

2 changes: 1 addition & 1 deletion docs/rules/UndefOr.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ See also:
- [NotBlank](NotBlank.md)
- [NotEmpty](NotEmpty.md)
- [NotUndef](NotUndef.md)
- [NullOr](NullOr.md)
- [NullType](NullType.md)
- [Nullable](Nullable.md)
23 changes: 23 additions & 0 deletions library/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ public function withNextSibling(Result $nextSibling): self
return $this->clone(nextSibling: $nextSibling);
}

public function withInvertedValidation(): self
{
return $this->clone(
isValid: !$this->isValid,
nextSibling: $this->nextSibling?->withInvertedValidation(),
children: array_map(static fn (Result $child) => $child->withInvertedValidation(), $this->children),
);
}

public function withInvertedMode(): self
{
return $this->clone(
Expand Down Expand Up @@ -153,6 +162,20 @@ public function isAlwaysVisible(): bool
return count($childrenAlwaysVisible) !== 1;
}

public function isSiblingCompatible(): bool
{
if ($this->children === [] && !$this->hasCustomTemplate()) {
return true;
}

$siblingCompatibleChildren = array_filter(
$this->children,
static fn (Result $child) => $child->isSiblingCompatible()
);

return count($siblingCompatibleChildren) === 1;
}

/**
* @param array<Result>|null $children
*/
Expand Down
34 changes: 20 additions & 14 deletions library/Rules/NullOr.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,36 @@
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\Wrapper;

use function array_map;

#[Template(
'The value must be null',
'The value must not be null',
self::TEMPLATE_STANDARD,
)]
#[Template(
'{{name}} must be null',
'{{name}} must not be null',
self::TEMPLATE_NAMED,
'or must be null',
'and must not be null',
)]
final class NullOr extends Wrapper
{
public const TEMPLATE_NAMED = '__named__';

public function evaluate(mixed $input): Result
{
$result = $this->rule->evaluate($input);
if ($input !== null) {
return $this->rule->evaluate($input)->withPrefixedId('nullOr');
return $this->enrichResult($result);
}

if ($this->getName()) {
return Result::passed($input, $this, [], self::TEMPLATE_NAMED);
if (!$result->isValid) {
return $this->enrichResult($result->withInvertedValidation());
}

return $this->enrichResult($result);
}

private function enrichResult(Result $result): Result
{
if ($result->isSiblingCompatible()) {
return $result
->withPrefixedId('nullOr')
->withNextSibling(new Result($result->isValid, $result->input, $this));
}

return Result::passed($input, $this);
return $result->withChildren(...array_map(fn(Result $child) => $this->enrichResult($child), $result->children));
}
}
91 changes: 70 additions & 21 deletions tests/integration/rules/nullOr.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -13,63 +13,80 @@ run([
'Inverted nullined, not name' => [v::not(v::nullOr(v::alpha()))->setName('Not'), null],
'With template' => [v::nullOr(v::alpha()), 123, 'Nine nimble numismatists near Naples'],
'With array template' => [v::nullOr(v::alpha()), 123, ['nullOrAlpha' => 'Next to nifty null notations']],
'Inverted nullined with template' => [
v::not(v::nullOr(v::alpha())),
null,
['notNullOrAlpha' => 'Next to nifty null notations'],
],
'Not a sibling compatible rule' => [
v::nullOr(v::alpha()->stringType()),
1234,
],
'Not a sibling compatible rule with templates' => [
v::nullOr(v::alpha()->stringType()),
1234,
[
'nullOrAlpha' => 'Should be nul or alpha',
'nullOrStringType' => 'Should be nul or string type',
],
],
]);
?>
--EXPECT--
Default
⎺⎺⎺⎺⎺⎺⎺
1234 must contain only letters (a-z)
- 1234 must contain only letters (a-z)
1234 must contain only letters (a-z) or must be null
- 1234 must contain only letters (a-z) or must be null
[
'nullOrAlpha' => '1234 must contain only letters (a-z)',
'nullOrAlpha' => '1234 must contain only letters (a-z) or must be null',
]

Inverted wrapper
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
"alpha" must not contain letters (a-z)
- "alpha" must not contain letters (a-z)
"alpha" must not contain letters (a-z) and must not be null
- "alpha" must not contain letters (a-z) and must not be null
[
'notNullOrAlpha' => '"alpha" must not contain letters (a-z)',
'notNullOrAlpha' => '"alpha" must not contain letters (a-z) and must not be null',
]

Inverted wrapped
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
"alpha" must not contain letters (a-z)
- "alpha" must not contain letters (a-z)
"alpha" must not contain letters (a-z) or must be null
- "alpha" must not contain letters (a-z) or must be null
[
'nullOrNotAlpha' => '"alpha" must not contain letters (a-z)',
'nullOrNotAlpha' => '"alpha" must not contain letters (a-z) or must be null',
]

Inverted nullined
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
The value must not be null
- The value must not be null
`null` must not contain letters (a-z) and must not be null
- `null` must not contain letters (a-z) and must not be null
[
'notNullOr' => 'The value must not be null',
'notNullOrAlpha' => '`null` must not contain letters (a-z) and must not be null',
]

Inverted nullined, wrapped name
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must not be null
- Wrapped must not be null
Wrapped must not contain letters (a-z) and must not be null
- Wrapped must not contain letters (a-z) and must not be null
[
'notNullOr' => 'Wrapped must not be null',
'notNullOrAlpha' => 'Wrapped must not contain letters (a-z) and must not be null',
]

Inverted nullined, wrapper name
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapper must not be null
- Wrapper must not be null
Wrapper must not contain letters (a-z) and must not be null
- Wrapper must not contain letters (a-z) and must not be null
[
'notNullOr' => 'Wrapper must not be null',
'notNullOrAlpha' => 'Wrapper must not contain letters (a-z) and must not be null',
]

Inverted nullined, not name
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Not must not be null
- Not must not be null
Not must not contain letters (a-z) and must not be null
- Not must not contain letters (a-z) and must not be null
[
'notNullOr' => 'Not must not be null',
'notNullOrAlpha' => 'Not must not contain letters (a-z) and must not be null',
]

With template
Expand All @@ -87,3 +104,35 @@ Next to nifty null notations
[
'nullOrAlpha' => 'Next to nifty null notations',
]

Inverted nullined with template
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Next to nifty null notations
- Next to nifty null notations
[
'notNullOrAlpha' => 'Next to nifty null notations',
]

Not a sibling compatible rule
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
1234 must contain only letters (a-z) or must be null
- All of the required rules must pass for 1234
- 1234 must contain only letters (a-z) or must be null
- 1234 must be of type string or must be null
[
'__root__' => 'All of the required rules must pass for 1234',
'nullOrAlpha' => '1234 must contain only letters (a-z) or must be null',
'nullOrStringType' => '1234 must be of type string or must be null',
]

Not a sibling compatible rule with templates
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Should be nul or alpha
- All of the required rules must pass for 1234
- Should be nul or alpha
- Should be nul or string type
[
'__root__' => 'All of the required rules must pass for 1234',
'nullOrAlpha' => 'Should be nul or alpha',
'nullOrStringType' => 'Should be nul or string type',
]
6 changes: 3 additions & 3 deletions tests/integration/transformers/prefix.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ not

nullOr
⎺⎺⎺⎺⎺⎺
"string" must be of type boolean
- "string" must be of type boolean
"string" must be of type boolean or must be null
- "string" must be of type boolean or must be null
[
'nullOrBoolType' => '"string" must be of type boolean',
'nullOrBoolType' => '"string" must be of type boolean or must be null',
]

property
Expand Down
Loading

0 comments on commit d1e0c8b

Please sign in to comment.