Skip to content

Commit

Permalink
Update UndefOr 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 "UndefOr", allowing it to
create a result with a sibling when possible. That will improve the
clarity of the error message.

[1]: 238f2d5
  • Loading branch information
henriquemoody committed Dec 4, 2024
1 parent d1e0c8b commit eb459ad
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 85 deletions.
38 changes: 31 additions & 7 deletions docs/rules/UndefOr.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

- `UndefOr(Validatable $rule)`

Validates if the given input is undefined or not.
Validates the input using a defined rule when the input is not `null` or an empty string (`''`).

By _undefined_ we consider `null` or an empty string (`''`), which implies that the input is not set. This is particularly useful when validating form fields
This rule can be particularly useful when validating form fields.

## Usage

```php
v::undefOr(v::alpha())->isValid(''); // true
Expand All @@ -14,7 +16,7 @@ v::undefOr(v::alpha())->isValid('username'); // true
v::undefOr(v::alpha())->isValid('has1number'); // false
```

## Note
## Prefix

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

Expand All @@ -23,16 +25,38 @@ v::undefOrEmail()->isValid('not an email'); // false
v::undefOrBetween(1, 3)->isValid(2); // true
```

## Templates

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

The templates from this rule serve as message suffixes:

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

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

## Template placeholders

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

## Categorization

- Nesting

## Changelog

| Version | Description |
|--------:|--------------------------------------|
| 3.0.0 | Renamed from "Optional" to "UndefOr" |
| 1.0.0 | Created |
| Version | Description |
|--------:|-----------------------|
| 3.0.0 | Renamed to `UndefOr` |
| 1.0.0 | Created as `Optional` |

***
See also:
Expand Down
34 changes: 20 additions & 14 deletions library/Rules/UndefOr.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,38 @@
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\Wrapper;

use function array_map;

#[Template(
'The value must be undefined',
'The value must not be undefined',
self::TEMPLATE_STANDARD,
)]
#[Template(
'{{name}} must be undefined',
'{{name}} must not be undefined',
self::TEMPLATE_NAMED,
'or must be undefined',
'and must not be undefined',
)]
final class UndefOr extends Wrapper
{
use CanValidateUndefined;

public const TEMPLATE_NAMED = '__named__';

public function evaluate(mixed $input): Result
{
$result = $this->rule->evaluate($input);
if (!$this->isUndefined($input)) {
return $this->rule->evaluate($input)->withPrefixedId('undefOr');
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('undefOr')
->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/undefOr.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -13,63 +13,80 @@ run([
'Inverted undefined, not name' => [v::not(v::undefOr(v::alpha()))->setName('Not'), null],
'With template' => [v::undefOr(v::alpha()), 123, 'Underneath the undulating umbrella'],
'With array template' => [v::undefOr(v::alpha()), 123, ['undefOrAlpha' => 'Undefined number of unique unicorns']],
'Inverted undefined with template' => [
v::not(v::undefOr(v::alpha())),
'',
['notUndefOrAlpha' => 'Should not be undefined or alpha'],
],
'Not a sibling compatible rule' => [
v::undefOr(v::alpha()->stringType()),
1234,
],
'Not a sibling compatible rule with templates' => [
v::undefOr(v::alpha()->stringType()),
1234,
[
'undefOrAlpha' => 'Should be nul or alpha',
'undefOrStringType' => '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 undefined
- 1234 must contain only letters (a-z) or must be undefined
[
'undefOrAlpha' => '1234 must contain only letters (a-z)',
'undefOrAlpha' => '1234 must contain only letters (a-z) or must be undefined',
]

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 undefined
- "alpha" must not contain letters (a-z) and must not be undefined
[
'notUndefOrAlpha' => '"alpha" must not contain letters (a-z)',
'notUndefOrAlpha' => '"alpha" must not contain letters (a-z) and must not be undefined',
]

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 undefined
- "alpha" must not contain letters (a-z) or must be undefined
[
'undefOrNotAlpha' => '"alpha" must not contain letters (a-z)',
'undefOrNotAlpha' => '"alpha" must not contain letters (a-z) or must be undefined',
]

Inverted undefined
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
The value must not be undefined
- The value must not be undefined
`null` must not contain letters (a-z) and must not be undefined
- `null` must not contain letters (a-z) and must not be undefined
[
'notUndefOr' => 'The value must not be undefined',
'notUndefOrAlpha' => '`null` must not contain letters (a-z) and must not be undefined',
]

Inverted undefined, wrapped name
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapped must not be undefined
- Wrapped must not be undefined
Wrapped must not contain letters (a-z) and must not be undefined
- Wrapped must not contain letters (a-z) and must not be undefined
[
'notUndefOr' => 'Wrapped must not be undefined',
'notUndefOrAlpha' => 'Wrapped must not contain letters (a-z) and must not be undefined',
]

Inverted undefined, wrapper name
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Wrapper must not be undefined
- Wrapper must not be undefined
Wrapper must not contain letters (a-z) and must not be undefined
- Wrapper must not contain letters (a-z) and must not be undefined
[
'notUndefOr' => 'Wrapper must not be undefined',
'notUndefOrAlpha' => 'Wrapper must not contain letters (a-z) and must not be undefined',
]

Inverted undefined, not name
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Not must not be undefined
- Not must not be undefined
Not must not contain letters (a-z) and must not be undefined
- Not must not contain letters (a-z) and must not be undefined
[
'notUndefOr' => 'Not must not be undefined',
'notUndefOrAlpha' => 'Not must not contain letters (a-z) and must not be undefined',
]

With template
Expand All @@ -87,3 +104,35 @@ Undefined number of unique unicorns
[
'undefOrAlpha' => 'Undefined number of unique unicorns',
]

Inverted undefined with template
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
Should not be undefined or alpha
- Should not be undefined or alpha
[
'notUndefOrAlpha' => 'Should not be undefined or alpha',
]

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

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',
'undefOrAlpha' => 'Should be nul or alpha',
'undefOrStringType' => 'Should be nul or string type',
]
6 changes: 3 additions & 3 deletions tests/integration/transformers/aliases.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ run([
--EXPECT--
optional
⎺⎺⎺⎺⎺⎺⎺⎺
`[]` must be a scalar value
- `[]` must be a scalar value
`[]` must be a scalar value or must be undefined
- `[]` must be a scalar value or must be undefined
[
'undefOrScalarVal' => '`[]` must be a scalar value',
'undefOrScalarVal' => '`[]` must be a scalar value or must be undefined',
]
6 changes: 3 additions & 3 deletions tests/integration/transformers/prefix.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ foo must be between 1 and 3

undefOr
⎺⎺⎺⎺⎺⎺⎺
"string" must be a URL
- "string" must be a URL
"string" must be a URL or must be undefined
- "string" must be a URL or must be undefined
[
'undefOrUrl' => '"string" must be a URL',
'undefOrUrl' => '"string" must be a URL or must be undefined',
]
41 changes: 4 additions & 37 deletions tests/unit/Rules/UndefOrTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\Rules\Stub;
use Respect\Validation\Test\RuleTestCase;
use stdClass;
Expand All @@ -20,45 +19,13 @@
#[CoversClass(UndefOr::class)]
final class UndefOrTest extends RuleTestCase
{
#[Test]
public function itShouldUseStandardTemplateWhenItHasNameWhenInputIsOptional(): void
{
$rule = new UndefOr(Stub::pass(1));

$result = $rule->evaluate('');

self::assertSame($rule, $result->rule);
self::assertSame(UndefOr::TEMPLATE_STANDARD, $result->template);
}

#[Test]
public function itShouldUseNamedTemplateWhenItHasNameWhenInputIsOptional(): void
{
$rule = new UndefOr(Stub::pass(1));
$rule->setName('foo');

$result = $rule->evaluate('');

self::assertSame($rule, $result->rule);
self::assertSame(UndefOr::TEMPLATE_NAMED, $result->template);
}

#[Test]
public function itShouldUseWrappedRuleToEvaluateWhenNotUndef(): void
{
$input = new stdClass();

$wrapped = Stub::pass(2);
$rule = new UndefOr($wrapped);

self::assertEquals($wrapped->evaluate($input)->withPrefixedId('undefOr'), $rule->evaluate($input));
}

/** @return iterable<string, array{UndefOr, mixed}> */
public static function providerForValidInput(): iterable
{
yield 'null' => [new UndefOr(Stub::daze()), null];
yield 'empty string' => [new UndefOr(Stub::daze()), ''];
yield 'null' => [new UndefOr(Stub::pass(1)), null];
yield 'empty string' => [new UndefOr(Stub::pass(1)), ''];
yield 'null with failing rule' => [new UndefOr(Stub::fail(1)), null];
yield 'empty string with failing rule' => [new UndefOr(Stub::fail(1)), ''];
yield 'not optional' => [new UndefOr(Stub::pass(1)), 42];
}

Expand Down

0 comments on commit eb459ad

Please sign in to comment.