Skip to content

Commit 48e0082

Browse files
committed
Trace paths instead of IDs
1 parent 901774f commit 48e0082

File tree

11 files changed

+198
-67
lines changed

11 files changed

+198
-67
lines changed

library/FailedResultIterator.php

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
7+
* SPDX-License-Identifier: MIT
8+
*/
9+
10+
namespace Respect\Validation;
11+
12+
use Countable;
13+
use Iterator;
14+
15+
use RecursiveIterator;
16+
use function array_filter;
17+
use function array_key_exists;
18+
use function array_map;
19+
use function array_values;
20+
use function count;
21+
use function current;
22+
use function key;
23+
use function next;
24+
use function reset;
25+
26+
/**
27+
* @implements Iterator<int, Result>
28+
*/
29+
final class FailedResultIterator implements Iterator, Countable, \RecursiveIterator
30+
{
31+
private array $children;
32+
33+
public function __construct(
34+
private readonly Result $result,
35+
) {
36+
$this->children = $this->extractDeduplicatedChildren();
37+
}
38+
39+
public function extractDeduplicatedChildren(): array
40+
{
41+
/** @var array<string, Result> $deduplicatedResults */
42+
$deduplicatedResults = [];
43+
$duplicateCounters = [];
44+
foreach ($this->result->children as $child) {
45+
if ($child->path !== null) {
46+
$deduplicatedResults[$child->path] = $child->isValid ? null : $child;
47+
continue;
48+
}
49+
50+
$id = $child->id;
51+
if (isset($duplicateCounters[$id])) {
52+
$id .= '.' . ++$duplicateCounters[$id];
53+
} elseif (array_key_exists($id, $deduplicatedResults)) {
54+
$deduplicatedResults[$id . '.1'] = $deduplicatedResults[$id]?->withId($id . '.1');
55+
unset($deduplicatedResults[$id]);
56+
$duplicateCounters[$id] = 2;
57+
$id .= '.2';
58+
}
59+
60+
$deduplicatedResults[$id] = $child->isValid ? null : $child->withId($id);
61+
}
62+
63+
return array_map(
64+
function (Result $child): Result {
65+
if ($this->result->path !== null && $child->path !== null && $child->path !== $this->result->path) {
66+
return $child->withPath($this->result->path);
67+
}
68+
69+
if ($this->result->path !== null && $child->path === null) {
70+
return $child->withPath($this->result->path);
71+
}
72+
73+
return $child;
74+
},
75+
array_values(array_filter($deduplicatedResults))
76+
);
77+
}
78+
79+
public function current(): Result|false
80+
{
81+
return current($this->children);
82+
}
83+
84+
public function getArrayCopy(): array
85+
{
86+
return $this->children;
87+
}
88+
89+
public function next(): void
90+
{
91+
next($this->children);
92+
}
93+
94+
public function key(): ?int
95+
{
96+
return key($this->children);
97+
}
98+
99+
public function valid(): bool
100+
{
101+
return key($this->children) !== null;
102+
}
103+
104+
public function rewind(): void
105+
{
106+
reset($this->children);
107+
}
108+
109+
public function count(): int
110+
{
111+
return count($this->children);
112+
}
113+
114+
public function hasChildren(): bool
115+
{
116+
return $this->result->children !== [];
117+
}
118+
119+
public function getChildren(): ?RecursiveIterator
120+
{
121+
if (!$this->hasChildren()) {
122+
return null;
123+
}
124+
125+
return new self($this->result);
126+
}
127+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Respect\Validation\Message\Placeholder;
11+
12+
final class Path
13+
{
14+
public function __construct(
15+
private readonly int|string $value
16+
) {
17+
}
18+
19+
public function getValue(): int|string
20+
{
21+
return $this->value;
22+
}
23+
}

library/Message/StandardFormatter.php

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@
1111

1212
use Respect\Validation\Exceptions\ComponentException;
1313
use Respect\Validation\Result;
14+
use Respect\Validation\FailedResultIterator;
1415

1516
use function array_filter;
16-
use function array_key_exists;
17-
use function array_map;
1817
use function array_reduce;
19-
use function array_values;
2018
use function count;
2119
use function current;
2220
use function is_array;
@@ -42,8 +40,8 @@ public function main(Result $result, array $templates, Translator $translator):
4240
{
4341
$selectedTemplates = $this->selectTemplates($result, $templates);
4442
if (!$this->isFinalTemplate($result, $selectedTemplates)) {
45-
foreach ($this->extractDeduplicatedChildren($result) as $child) {
46-
return $this->main($this->resultWithPath($result, $child), $selectedTemplates, $translator);
43+
foreach (new FailedResultIterator($result) as $child) {
44+
return $this->main($child, $selectedTemplates, $translator);
4745
}
4846
}
4947

@@ -78,17 +76,14 @@ public function full(
7876
}
7977

8078
if (!$isFinalTemplate) {
81-
$results = array_map(
82-
fn(Result $child) => $this->resultWithPath($result, $child),
83-
$this->extractDeduplicatedChildren($result)
84-
);
79+
$results = new FailedResultIterator($result);
8580
foreach ($results as $child) {
8681
$rendered .= $this->full(
8782
$child,
8883
$selectedTemplates,
8984
$translator,
9085
$depth,
91-
...array_filter($results, static fn (Result $sibling) => $sibling !== $child)
86+
...array_filter($results->getArrayCopy(), static fn (Result $sibling) => $sibling !== $child)
9287
);
9388
$rendered .= PHP_EOL;
9489
}
@@ -105,7 +100,7 @@ public function full(
105100
public function array(Result $result, array $templates, Translator $translator): array
106101
{
107102
$selectedTemplates = $this->selectTemplates($result, $templates);
108-
$deduplicatedChildren = $this->extractDeduplicatedChildren($result);
103+
$deduplicatedChildren = new FailedResultIterator($result);
109104
if (count($deduplicatedChildren) === 0 || $this->isFinalTemplate($result, $selectedTemplates)) {
110105
return [
111106
$result->getDeepestPath() ?? $result->id => $this->renderer->render(
@@ -256,32 +251,4 @@ private function selectTemplates(Result $result, array $templates): array
256251

257252
return $templates;
258253
}
259-
260-
/** @return array<Result> */
261-
private function extractDeduplicatedChildren(Result $result): array
262-
{
263-
/** @var array<string, Result> $deduplicatedResults */
264-
$deduplicatedResults = [];
265-
$duplicateCounters = [];
266-
foreach ($result->children as $child) {
267-
$id = $child->getDeepestPath() ?? $child->id;
268-
if (isset($duplicateCounters[$id])) {
269-
$id .= '.' . ++$duplicateCounters[$id];
270-
} elseif (array_key_exists($id, $deduplicatedResults)) {
271-
$deduplicatedResults[$id . '.1'] = $deduplicatedResults[$id]?->withId($id . '.1');
272-
unset($deduplicatedResults[$id]);
273-
$duplicateCounters[$id] = 2;
274-
$id .= '.2';
275-
}
276-
277-
if ($child->path === null) {
278-
$deduplicatedResults[$id] = $child->isValid ? null : $child->withId($id);
279-
continue;
280-
}
281-
282-
$deduplicatedResults[$id] = $child->isValid ? null : $child;
283-
}
284-
285-
return array_values(array_filter($deduplicatedResults));
286-
}
287254
}

library/Message/StandardRenderer.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use ReflectionClass;
1313
use Respect\Stringifier\Stringifier;
1414
use Respect\Validation\Message\Placeholder\Listed;
15+
use Respect\Validation\Message\Placeholder\Path;
1516
use Respect\Validation\Message\Placeholder\Quoted;
1617
use Respect\Validation\Result;
1718
use Respect\Validation\Rule;
@@ -36,7 +37,7 @@ public function __construct(
3637
public function render(Result $result, Translator $translator, ?string $template = null): string
3738
{
3839
$parameters = $result->parameters;
39-
$parameters['path'] = $result->path !== null ? Quoted::fromPath($result->path) : null;
40+
$parameters['path'] = $result->path !== null ? new Path($result->path) : null;
4041
$parameters['input'] = $result->input;
4142

4243
$builtName = $result->name ?? $parameters['path'] ?? $this->placeholder('input', $result->input, $translator);

library/Message/StandardStringifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Respect\Stringifier\Stringifiers\StringableObjectStringifier;
3333
use Respect\Stringifier\Stringifiers\ThrowableObjectStringifier;
3434
use Respect\Validation\Message\Stringifier\ListedStringifier;
35+
use Respect\Validation\Message\Stringifier\PathStringifier;
3536
use Respect\Validation\Message\Stringifier\QuotedStringifier;
3637

3738
final class StandardStringifier implements Stringifier
@@ -88,6 +89,7 @@ private function createStringifier(Quoter $quoter): Stringifier
8889
$stringifier->prependStringifier(new ThrowableObjectStringifier($jsonEncodableStringifier, $quoter));
8990
$stringifier->prependStringifier(new DateTimeStringifier($quoter, DateTimeInterface::ATOM));
9091
$stringifier->prependStringifier(new IteratorObjectStringifier($stringifier, $quoter));
92+
$stringifier->prependStringifier(new PathStringifier($quoter));
9193
$stringifier->prependStringifier(new QuotedStringifier($quoter));
9294
$stringifier->prependStringifier(new ListedStringifier($stringifier));
9395

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
7+
* SPDX-License-Identifier: MIT
8+
*/
9+
10+
namespace Respect\Validation\Message\Stringifier;
11+
12+
use Respect\Stringifier\Quoter;
13+
use Respect\Stringifier\Stringifier;
14+
use Respect\Validation\Message\Placeholder\Path;
15+
16+
final class PathStringifier implements Stringifier
17+
{
18+
public function __construct(
19+
private readonly Quoter $quoter
20+
) {
21+
}
22+
23+
public function stringify(mixed $raw, int $depth): ?string
24+
{
25+
if (!$raw instanceof Path) {
26+
return null;
27+
}
28+
29+
return $this->quoter->quote('.' . $raw->getValue(), $depth);
30+
}
31+
}

library/Transformers/Deprecated/KeyValueRule.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
namespace Respect\Validation\Transformers\Deprecated;
1111

12-
use Respect\Validation\Message\Placeholder\Quoted;
12+
use Respect\Validation\Message\Placeholder\Path;
1313
use Respect\Validation\Rules\AlwaysInvalid;
1414
use Respect\Validation\Rules\Key;
1515
use Respect\Validation\Rules\KeyExists;
@@ -56,7 +56,7 @@ static function ($input) use ($comparedKey, $ruleName, $baseKey) {
5656
return new Templated(
5757
new AlwaysInvalid(),
5858
'{{baseKey}} must be valid to validate {{comparedKey}}',
59-
['comparedKey' => Quoted::fromPath($comparedKey), 'baseKey' => Quoted::fromPath($baseKey)]
59+
['comparedKey' => new Path($comparedKey), 'baseKey' => new Path($baseKey)],
6060
);
6161
}
6262
}

tests/feature/Issues/Issue1289Test.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,7 @@
5656
[
5757
0 => [
5858
'__root__' => '`.0` must pass the rules',
59-
'default' => [
60-
'__root__' => '`.default` must pass one of the rules',
61-
'stringType' => '`.default` must be a string',
62-
'boolType' => '`.default` must be a boolean',
63-
],
59+
'default' => '`.default` must be a boolean',
6460
'description' => '`.description` must be a string value',
6561
],
6662
],

tests/feature/Issues/Issue1376Test.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,7 @@
2828
'__root__' => '`stdClass { +$author="foo" }` must pass all the rules',
2929
'title' => '`.title` must be present',
3030
'description' => '`.description` must be present',
31-
'author' => [
32-
'__root__' => '`.author` must pass all the rules',
33-
'intType' => '`.author` must be an integer',
34-
'lengthBetween' => 'The length of `.author` must be between 1 and 2',
35-
],
31+
'author' => 'The length of `.author` must be between 1 and 2',
3632
'user' => '`.user` must be present',
3733
],
3834
));

tests/feature/Rules/AttributesTest.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,7 @@
5252
[
5353
'__root__' => '`Respect\Validation\Test\Stubs\WithAttributes { +$name="" +$birthdate="not a date" +$email="not an email" +$phone ... }` must pass the rules',
5454
'name' => '`.name` must not be empty',
55-
'birthdate' => [
56-
'__root__' => '`.birthdate` must pass all the rules',
57-
'date' => '`.birthdate` must be a valid date in the format "2005-12-30"',
58-
'dateTimeDiffLessThanOrEqual' => 'For comparison with now, `.birthdate` must be a valid datetime',
59-
],
55+
'birthdate' => 'For comparison with now, `.birthdate` must be a valid datetime',
6056
'email' => '`.email` must be a valid email address or must be null',
6157
'phone' => '`.phone` must be a valid telephone number or must be null',
6258
],

0 commit comments

Comments
 (0)