Skip to content

Commit 640b03c

Browse files
committed
Fix input overwrite not propagating to adjacent results
Adjacent results are results that treat the same input. When overwriting the input of a result, we should also overwrite the input of its adjacent result to maintain consistency. Currently, there are no cases where this has caused issues, but this change prevents potential problems. Assisted-by: Claude Code (Opus 4.5)
1 parent ec16b3d commit 640b03c

2 files changed

Lines changed: 330 additions & 1 deletion

File tree

src/Result.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public function asAdjacentOf(Result $result, string $prefix): Result
7777
if ($this->allowsAdjacent()) {
7878
return clone ($result, [
7979
'id' => $this->id->withPrefix($prefix),
80-
'adjacent' => clone($this, ['input' => $result->input]),
80+
'adjacent' => $this->withInput($result->input),
8181
]);
8282
}
8383

@@ -185,6 +185,18 @@ public function withAdjacent(Result $adjacent): self
185185
return clone($this, ['adjacent' => $adjacent]);
186186
}
187187

188+
public function withInput(mixed $input): self
189+
{
190+
return clone($this, [
191+
'input' => $input,
192+
'adjacent' => $this->adjacent?->withInput($input),
193+
'children' => $this->mapChildrenIf(
194+
fn(Result $child) => $child->input === $this->input && $child->path === $this->path,
195+
static fn(Result $child) => $child->withInput($input),
196+
),
197+
]);
198+
}
199+
188200
public function withToggledValidation(): self
189201
{
190202
return clone($this, [
@@ -227,4 +239,14 @@ private function mapChildren(callable $callback): array
227239
{
228240
return $this->children === [] ? [] : array_map($callback, $this->children);
229241
}
242+
243+
/** @return array<Result> */
244+
private function mapChildrenIf(callable $condition, callable $callback): array
245+
{
246+
if ($this->children === []) {
247+
return [];
248+
}
249+
250+
return array_map(fn (self $child) => $condition($child) ? $callback : $child, $this->children);
251+
}
230252
}

tests/unit/ResultTest.php

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Validation;
12+
13+
use PHPUnit\Framework\Attributes\CoversClass;
14+
use PHPUnit\Framework\Attributes\Group;
15+
use PHPUnit\Framework\Attributes\Test;
16+
use Respect\Validation\Test\Builders\ResultBuilder;
17+
use Respect\Validation\Test\TestCase;
18+
19+
#[Group('core')]
20+
#[CoversClass(Result::class)]
21+
final class ResultTest extends TestCase
22+
{
23+
#[Test]
24+
public function itShouldUpdateInputWhenWithInputIsCalled(): void
25+
{
26+
$originalInput = 'original';
27+
$newInput = 'updated';
28+
29+
$result = (new ResultBuilder())
30+
->input($originalInput)
31+
->build();
32+
33+
$updatedResult = $result->withInput($newInput);
34+
35+
self::assertSame($newInput, $updatedResult->input);
36+
}
37+
38+
#[Test]
39+
public function itShouldUpdateAdjacentInputWhenWithInputIsCalled(): void
40+
{
41+
$originalInput = 'original';
42+
$newInput = 'updated';
43+
44+
$adjacent = (new ResultBuilder())
45+
->input($originalInput)
46+
->build();
47+
48+
$result = (new ResultBuilder())
49+
->input($originalInput)
50+
->adjacent($adjacent)
51+
->build();
52+
53+
$updatedResult = $result->withInput($newInput);
54+
55+
self::assertSame($newInput, $updatedResult->input);
56+
self::assertSame($newInput, $updatedResult->adjacent?->input);
57+
self::assertSame($originalInput, $result->adjacent?->input);
58+
}
59+
60+
#[Test]
61+
public function itShouldUpdateChildrenInputWhenWithInputIsCalledAndChildHasSameInputAndPath(): void
62+
{
63+
$originalInput = 'original';
64+
$newInput = 'updated';
65+
$path = new Path('parent');
66+
67+
$child = (new ResultBuilder())
68+
->input($originalInput)
69+
->path($path)
70+
->build();
71+
72+
$result = (new ResultBuilder())
73+
->input($originalInput)
74+
->path($path)
75+
->children($child)
76+
->build();
77+
78+
$updatedResult = $result->withInput($newInput);
79+
80+
self::assertSame($newInput, $updatedResult->input);
81+
self::assertSame($newInput, $updatedResult->children[0]->input);
82+
self::assertSame($originalInput, $result->children[0]->input);
83+
}
84+
85+
#[Test]
86+
public function itShouldUpdateChildrenInputWhenWithInputIsCalledAndBothHaveNullPath(): void
87+
{
88+
$originalInput = 'original';
89+
$newInput = 'updated';
90+
91+
$child = (new ResultBuilder())
92+
->input($originalInput)
93+
->build();
94+
95+
$result = (new ResultBuilder())
96+
->input($originalInput)
97+
->children($child)
98+
->build();
99+
100+
$updatedResult = $result->withInput($newInput);
101+
102+
self::assertSame($newInput, $updatedResult->input);
103+
self::assertSame($newInput, $updatedResult->children[0]->input);
104+
}
105+
106+
#[Test]
107+
public function itShouldNotUpdateChildrenInputWhenWithInputIsCalledAndChildHasDifferentInput(): void
108+
{
109+
$originalInput = 'original';
110+
$newInput = 'updated';
111+
$childInput = 'different';
112+
113+
$child = (new ResultBuilder())
114+
->input($childInput)
115+
->path(new Path('parent'))
116+
->build();
117+
118+
$result = (new ResultBuilder())
119+
->input($originalInput)
120+
->path(new Path('parent'))
121+
->children($child)
122+
->build();
123+
124+
$updatedResult = $result->withInput($newInput);
125+
126+
self::assertSame($newInput, $updatedResult->input);
127+
self::assertSame($childInput, $updatedResult->children[0]->input);
128+
}
129+
130+
#[Test]
131+
public function itShouldUpdateOnlyMatchingChildrenInputWhenWithInputIsCalled(): void
132+
{
133+
$originalInput = 'original';
134+
$newInput = 'updated';
135+
$differentInput = 'different';
136+
$path = new Path('parent');
137+
138+
$matchingChild = (new ResultBuilder())
139+
->input($originalInput)
140+
->path($path)
141+
->build();
142+
143+
$differentChild = (new ResultBuilder())
144+
->input($differentInput)
145+
->path($path)
146+
->build();
147+
148+
$result = (new ResultBuilder())
149+
->input($originalInput)
150+
->path($path)
151+
->children($matchingChild, $differentChild)
152+
->build();
153+
154+
$updatedResult = $result->withInput($newInput);
155+
156+
self::assertSame($newInput, $updatedResult->input);
157+
self::assertSame($newInput, $updatedResult->children[0]->input);
158+
self::assertSame($differentInput, $updatedResult->children[1]->input);
159+
}
160+
161+
#[Test]
162+
public function itShouldNotUpdateChildrenInputWhenWithInputIsCalledAndChildHasDifferentPath(): void
163+
{
164+
$originalInput = 'original';
165+
$newInput = 'updated';
166+
167+
$child = (new ResultBuilder())
168+
->input($originalInput)
169+
->path(new Path('child'))
170+
->build();
171+
172+
$result = (new ResultBuilder())
173+
->input($originalInput)
174+
->path(new Path('parent'))
175+
->children($child)
176+
->build();
177+
178+
$updatedResult = $result->withInput($newInput);
179+
180+
self::assertSame($newInput, $updatedResult->input);
181+
self::assertSame($originalInput, $updatedResult->children[0]->input);
182+
}
183+
184+
#[Test]
185+
public function itShouldUpdateInputAdjacentAndChildrenWithSameInputWhenWithInputIsCalled(): void
186+
{
187+
$originalInput = 'original';
188+
$newInput = 'updated';
189+
$path = new Path('parent');
190+
191+
$adjacent = (new ResultBuilder())
192+
->input($originalInput)
193+
->path($path)
194+
->build();
195+
196+
$child = (new ResultBuilder())
197+
->input($originalInput)
198+
->path($path)
199+
->build();
200+
201+
$result = (new ResultBuilder())
202+
->input($originalInput)
203+
->path($path)
204+
->adjacent($adjacent)
205+
->children($child)
206+
->build();
207+
208+
$updatedResult = $result->withInput($newInput);
209+
210+
self::assertSame($newInput, $updatedResult->input);
211+
self::assertNotNull($updatedResult->adjacent);
212+
self::assertSame($newInput, $updatedResult->adjacent->input);
213+
self::assertSame($newInput, $updatedResult->children[0]->input);
214+
}
215+
216+
#[Test]
217+
public function itShouldUpdateNestedChildrenInputWhenWithInputIsCalled(): void
218+
{
219+
$originalInput = 'original';
220+
$newInput = 'updated';
221+
$path = new Path('parent');
222+
223+
$grandchild = (new ResultBuilder())
224+
->input($originalInput)
225+
->path($path)
226+
->build();
227+
228+
$child = (new ResultBuilder())
229+
->input($originalInput)
230+
->path($path)
231+
->children($grandchild)
232+
->build();
233+
234+
$result = (new ResultBuilder())
235+
->input($originalInput)
236+
->path($path)
237+
->children($child)
238+
->build();
239+
240+
$updatedResult = $result->withInput($newInput);
241+
242+
self::assertSame($newInput, $updatedResult->input);
243+
self::assertSame($newInput, $updatedResult->children[0]->input);
244+
self::assertSame($newInput, $updatedResult->children[0]->children[0]->input);
245+
}
246+
247+
#[Test]
248+
public function itShouldNotUpdateNestedChildrenWhenWithInputIsCalledAndGrandchildHasDifferentPath(): void
249+
{
250+
$originalInput = 'original';
251+
$newInput = 'updated';
252+
$grandchildInput = 'grandchild_input';
253+
254+
$grandchild = (new ResultBuilder())
255+
->input($grandchildInput)
256+
->path(new Path('grandchild'))
257+
->build();
258+
259+
$child = (new ResultBuilder())
260+
->input($originalInput)
261+
->path(new Path('child'))
262+
->children($grandchild)
263+
->build();
264+
265+
$result = (new ResultBuilder())
266+
->input($originalInput)
267+
->path(new Path('parent'))
268+
->children($child)
269+
->build();
270+
271+
$updatedResult = $result->withInput($newInput);
272+
273+
self::assertSame($newInput, $updatedResult->input);
274+
self::assertSame($originalInput, $updatedResult->children[0]->input);
275+
self::assertSame($grandchildInput, $updatedResult->children[0]->children[0]->input);
276+
}
277+
278+
#[Test]
279+
public function itShouldUpdateNestedChildrenWhenWithInputIsCalledAndGrandchildHasSameInputAndPath(): void
280+
{
281+
$originalInput = 'original';
282+
$newInput = 'updated';
283+
284+
$grandchild = (new ResultBuilder())
285+
->input($originalInput)
286+
->path(new Path('grandchild'))
287+
->build();
288+
289+
$child = (new ResultBuilder())
290+
->input($originalInput)
291+
->path(new Path('child'))
292+
->children($grandchild)
293+
->build();
294+
295+
$result = (new ResultBuilder())
296+
->input($originalInput)
297+
->path(new Path('result'))
298+
->children($child)
299+
->build();
300+
301+
$updatedResult = $result->withInput($newInput);
302+
303+
self::assertSame($newInput, $updatedResult->input);
304+
self::assertSame($originalInput, $updatedResult->children[0]->input);
305+
self::assertSame($originalInput, $updatedResult->children[0]->children[0]->input);
306+
}
307+
}

0 commit comments

Comments
 (0)