Skip to content

Commit

Permalink
Handle siblings when creating messages
Browse files Browse the repository at this point in the history
The way we display messages could have been better, and it took me a
while to realise that to make it better, I would need to handle the
siblings of a result before deciding whether we should render it.

Another issue was that rules like Key and Property had to create a
"dumb" parent just so we would display the messages correctly, and in
some cases, that wasn't even enough.

This commit introduces quite a few changes to how the library works,
making the messages much more straightforward.
  • Loading branch information
henriquemoody committed Dec 13, 2024
1 parent 52e628f commit 82cb05b
Show file tree
Hide file tree
Showing 15 changed files with 193 additions and 72 deletions.
8 changes: 7 additions & 1 deletion library/Message/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ public function main(Result $result, array $templates, Translator $translator):
/**
* @param array<string, mixed> $templates
*/
public function full(Result $result, array $templates, Translator $translator, int $depth = 0): string;
public function full(
Result $result,
array $templates,
Translator $translator,
int $depth = 0,
Result ...$siblings
): string;

/**
* @param array<string, mixed> $templates
Expand Down
58 changes: 53 additions & 5 deletions library/Message/StandardFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use function array_filter;
use function array_key_exists;
use function array_reduce;
use function array_values;
use function count;
use function current;
Expand Down Expand Up @@ -51,13 +52,18 @@ public function main(Result $result, array $templates, Translator $translator):
/**
* @param array<string, mixed> $templates
*/
public function full(Result $result, array $templates, Translator $translator, int $depth = 0): string
{
public function full(
Result $result,
array $templates,
Translator $translator,
int $depth = 0,
Result ...$siblings
): string {
$selectedTemplates = $this->selectTemplates($result, $templates);
$isFinalTemplate = $this->isFinalTemplate($result, $selectedTemplates);

$rendered = '';
if ($result->isAlwaysVisible() || $isFinalTemplate) {
if ($this->isAlwaysVisible($result, ...$siblings) || $isFinalTemplate) {
$indentation = str_repeat(' ', $depth * 2);
$rendered .= sprintf(
'%s- %s' . PHP_EOL,
Expand All @@ -68,8 +74,15 @@ public function full(Result $result, array $templates, Translator $translator, i
}

if (!$isFinalTemplate) {
foreach ($this->extractDeduplicatedChildren($result) as $child) {
$rendered .= $this->full($child, $selectedTemplates, $translator, $depth);
$results = $this->extractDeduplicatedChildren($result);
foreach ($results as $child) {
$rendered .= $this->full(
$child,
$selectedTemplates,
$translator,
$depth,
...array_filter($results, static fn (Result $sibling) => $sibling !== $child)
);
$rendered .= PHP_EOL;
}
}
Expand Down Expand Up @@ -117,6 +130,41 @@ public function array(Result $result, array $templates, Translator $translator):
return $messages;
}

private function isAlwaysVisible(Result $result, Result ...$siblings): bool
{
if ($result->isValid) {
return false;
}

if ($result->hasCustomTemplate()) {
return true;
}

$childrenAlwaysVisible = array_filter(
$result->children,
fn (Result $child) => $this->isAlwaysVisible($child, ...array_filter(
$result->children,
static fn (Result $sibling) => $sibling !== $child
))
);
if (count($childrenAlwaysVisible) !== 1) {
return true;
}

if (count($siblings) === 0) {
return false;
}

return array_reduce(
$siblings,
fn (bool $carry, Result $currentSibling) => $carry || $this->isAlwaysVisible(
$currentSibling,
...array_filter($siblings, static fn (Result $sibling) => $sibling !== $currentSibling)
),
true
);
}

/** @param array<string, mixed> $templates */
private function getTemplated(Result $result, array $templates): Result
{
Expand Down
29 changes: 13 additions & 16 deletions library/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function __construct(
?string $name = null,
?string $id = null,
public readonly ?Result $subsequent = null,
public readonly bool $unchangeableId = false,
Result ...$children,
) {
$this->name = $rule->getName() ?? $name;
Expand Down Expand Up @@ -75,12 +76,21 @@ public function withTemplate(string $template): self

public function withId(string $id): self
{
if ($this->unchangeableId) {
return $this;
}

return $this->clone(id: $id);
}

public function withUnchangeableId(string $id): self
{
return $this->clone(id: $id, unchangeableId: true);
}

public function withPrefixedId(string $prefix): self
{
if ($this->id === $this->name) {
if ($this->id === $this->name || $this->unchangeableId) {
return $this;
}

Expand Down Expand Up @@ -142,21 +152,6 @@ public function hasCustomTemplate(): bool
return preg_match('/__[0-9a-z_]+_/', $this->template) === 0;
}

public function isAlwaysVisible(): bool
{
if ($this->isValid) {
return false;
}

if ($this->hasCustomTemplate()) {
return true;
}

$childrenAlwaysVisible = array_filter($this->children, static fn (Result $child) => $child->isAlwaysVisible());

return count($childrenAlwaysVisible) !== 1;
}

public function allowsSubsequent(): bool
{
if ($this->children === [] && !$this->hasCustomTemplate()) {
Expand All @@ -182,6 +177,7 @@ private function clone(
?string $name = null,
?string $id = null,
?Result $subsequent = null,
?bool $unchangeableId = null,
?array $children = null
): self {
return new self(
Expand All @@ -194,6 +190,7 @@ private function clone(
$name ?? $this->name,
$id ?? $this->id,
$subsequent ?? $this->subsequent,
$unchangeableId ?? $this->unchangeableId,
...($children ?? $this->children)
);
}
Expand Down
8 changes: 2 additions & 6 deletions library/Rules/Key.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,9 @@ public function evaluate(mixed $input): Result
return $keyExistsResult;
}

$child = $this->rule
return $this->rule
->evaluate($input[$this->key])
->withId((string) $this->key);

return (new Result($child->isValid, $input, $this))
->withChildren($child)
->withId((string) $this->key)
->withUnchangeableId((string) $this->key)
->withNameIfMissing($this->rule->getName() ?? (string) $this->key);
}
}
8 changes: 2 additions & 6 deletions library/Rules/KeyOptional.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,9 @@ public function evaluate(mixed $input): Result
return $keyExistsResult->withInvertedMode();
}

$child = $this->rule
return $this->rule
->evaluate($input[$this->key])
->withId((string) $this->key);

return (new Result($child->isValid, $input, $this))
->withChildren($child)
->withId((string) $this->key)
->withUnchangeableId((string) $this->key)
->withNameIfMissing($this->rule->getName() ?? (string) $this->key);
}
}
8 changes: 2 additions & 6 deletions library/Rules/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,9 @@ public function evaluate(mixed $input): Result
return $propertyExistsResult;
}

$childResult = $this->rule
return $this->rule
->evaluate($this->extractPropertyValue($input, $this->propertyName))
->withId($this->propertyName);

return (new Result($childResult->isValid, $input, $this))
->withChildren($childResult)
->withId($this->propertyName)
->withUnchangeableId($this->propertyName)
->withNameIfMissing($this->rule->getName() ?? $this->propertyName);
}
}
8 changes: 2 additions & 6 deletions library/Rules/PropertyOptional.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,9 @@ public function evaluate(mixed $input): Result
return $propertyExistsResult->withInvertedMode();
}

$childResult = $this->rule
return $this->rule
->evaluate($this->extractPropertyValue($input, $this->propertyName))
->withId($this->propertyName);

return (new Result($childResult->isValid, $input, $this))
->withChildren($childResult)
->withId($this->propertyName)
->withUnchangeableId($this->propertyName)
->withNameIfMissing($this->rule->getName() ?? $this->propertyName);
}
}
5 changes: 3 additions & 2 deletions tests/integration/issues/179.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ exceptionAll('https://github.com/Respect/Validation/issues/179', static fn() =>
https://github.com/Respect/Validation/issues/179
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
host must be a string
- host must be a string
- user must be present
- These rules must pass for Settings
- host must be a string
- user must be present
[
'__root__' => 'These rules must pass for Settings',
'host' => 'host must be a string',
Expand Down
3 changes: 1 addition & 2 deletions tests/integration/issues/446.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ exceptionAll('https://github.com/Respect/Validation/issues/446', static function
https://github.com/Respect/Validation/issues/446
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
The length of name must be between 2 and 32
- These rules must pass for `["name": "w", "email": "[email protected]"]`
- The length of name must be between 2 and 32
- The length of name must be between 2 and 32
[
'name' => 'The length of name must be between 2 and 32',
]
3 changes: 1 addition & 2 deletions tests/integration/issues/799.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ https://github.com/Respect/Validation/issues/799
https://github.com/Respect/Validation/issues/799
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
scheme must start with "https"
- These rules must pass for `["scheme": "http", "host": "www.google.com", "path": "/search", "query": "q=respect.github.com"]`
- scheme must start with "https"
- scheme must start with "https"
[
'scheme' => 'scheme must start with "https"',
]
1 change: 0 additions & 1 deletion tests/integration/issues/805.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ https://github.com/Respect/Validation/issues/805
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
WRONG EMAIL!!!!!!
- WRONG EMAIL!!!!!!
- WRONG EMAIL!!!!!!
[
'email' => 'WRONG EMAIL!!!!!!',
]
10 changes: 6 additions & 4 deletions tests/integration/rules/keySet.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,9 @@ foo must be present
multiple rules / two extra keys
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
qux must be an integer
- qux must be an integer
- baz must not be present
- `["foo": 42, "bar": "string", "baz": true, "qux": false]` contains extra keys
- qux must be an integer
- baz must not be present
[
'__root__' => '`["foo": 42, "bar": "string", "baz": true, "qux": false]` contains extra keys',
'qux' => 'qux must be an integer',
Expand All @@ -216,8 +217,9 @@ bar must be an integer
multiple rules / single missing key / single failed validation
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
bar must be an integer
- bar must be an integer
- baz must be present
- `["foo": 42, "bar": "string"]` contains missing keys
- bar must be an integer
- baz must be present
[
'__root__' => '`["foo": 42, "bar": "string"]` contains missing keys',
'bar' => 'bar must be an integer',
Expand Down
16 changes: 5 additions & 11 deletions tests/library/Builders/ResultBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ final class ResultBuilder

private ?Result $subsequent = null;

private bool $unchangeableId = false;

/** @var array<Result> */
private array $children = [];

Expand All @@ -55,22 +57,14 @@ public function build(): Result
$this->name,
$this->id,
$this->subsequent,
$this->unchangeableId,
...$this->children
);
}

public function isAlwaysVisible(): self
public function isValid(bool $isValid): self
{
return $this->withCustomTemplate();
}

public function isNotAlwaysVisible(): self
{
$this->template = 'Custom template';
$this->children = [
(new self())->withCustomTemplate()->build(),
(new self())->children((new self())->withCustomTemplate()->build())->build(),
];
$this->isValid = $isValid;

return $this;
}
Expand Down
Loading

0 comments on commit 82cb05b

Please sign in to comment.