Skip to content

Commit b57c21a

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

File tree

11 files changed

+183
-67
lines changed

11 files changed

+183
-67
lines changed

Diff for: library/Message/Placeholder/Path.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
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+
}

Diff for: library/Message/StandardFormatter.php

+6-39
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\ResultSet;
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 ResultSet($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 ResultSet($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 ResultSet($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
}

Diff for: library/Message/StandardRenderer.php

+2-1
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);

Diff for: library/Message/StandardStringifier.php

+2
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

Diff for: library/Message/Stringifier/PathStringifier.php

+31
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 <[email protected]>
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+
}

Diff for: library/ResultSet.php

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

Diff for: library/Transformers/Deprecated/KeyValueRule.php

+2-2
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
}

Diff for: tests/feature/Issues/Issue1289Test.php

+1-5
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
],

Diff for: tests/feature/Issues/Issue1376Test.php

+1-5
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
));

Diff for: tests/feature/Rules/AttributesTest.php

+1-5
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
],

Diff for: tests/feature/Rules/EachTest.php

+2-10
Original file line numberDiff line numberDiff line change
@@ -254,16 +254,8 @@
254254
FULL_MESSAGE,
255255
[
256256
'__root__' => 'Each item in `[2, 4]` must be valid',
257-
0 => [
258-
'__root__' => '`.0` must pass all the rules',
259-
'between' => '`.0` must be between 5 and 7',
260-
'odd' => '`.0` must be an odd number',
261-
],
262-
1 => [
263-
'__root__' => '`.1` must pass all the rules',
264-
'between' => '`.1` must be between 5 and 7',
265-
'odd' => '`.1` must be an odd number',
266-
],
257+
0 => '`.0` must be an odd number',
258+
1 => '`.1` must be an odd number',
267259
],
268260
));
269261

0 commit comments

Comments
 (0)