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)
This commit is contained in:
Henrique Moody 2026-01-26 12:41:43 +01:00
commit aafa204307
No known key found for this signature in database
GPG key ID: 221E9281655813A6
2 changed files with 338 additions and 3 deletions

View file

@ -77,7 +77,7 @@ final readonly class Result
if ($this->allowsAdjacent()) {
return clone ($result, [
'id' => $this->id->withPrefix($prefix),
'adjacent' => clone($this, ['input' => $result->input]),
'adjacent' => $this->withInput($result->input),
]);
}
@ -145,7 +145,10 @@ final readonly class Result
return clone ($this, [
'name' => null,
'adjacent' => $this->adjacent?->withoutName(),
'children' => $this->mapChildren(fn(Result $r) => $r->name === $this->name ? $r->withoutName() : $r),
'children' => $this->mapChildrenIf(
fn(Result $child) => $child->name === $this->name,
static fn(Result $child) => $child->withoutName(),
),
]);
}
@ -164,7 +167,10 @@ final readonly class Result
'name' => $name,
'hasPrecedentName' => $this->path === null,
'adjacent' => $this->adjacent?->withName($name),
'children' => $this->mapChildren(static fn(Result $r) => $r->name === null ? $r->withName($name) : $r),
'children' => $this->mapChildrenIf(
static fn(Result $child) => $child->name === null,
static fn(Result $child) => $child->withName($name),
),
]);
}
@ -185,6 +191,18 @@ final readonly class Result
return clone($this, ['adjacent' => $adjacent]);
}
public function withInput(mixed $input): self
{
return clone($this, [
'input' => $input,
'adjacent' => $this->adjacent?->withInput($input),
'children' => $this->mapChildrenIf(
fn(Result $child) => $child->input === $this->input && $child->path === $this->path,
static fn(Result $child) => $child->withInput($input),
),
]);
}
public function withToggledValidation(): self
{
return clone($this, [
@ -227,4 +245,14 @@ final readonly class Result
{
return $this->children === [] ? [] : array_map($callback, $this->children);
}
/** @return array<Result> */
private function mapChildrenIf(callable $condition, callable $callback): array
{
if ($this->children === []) {
return [];
}
return array_map(static fn(self $child) => $condition($child) ? $callback($child) : $child, $this->children);
}
}

307
tests/unit/ResultTest.php Normal file
View file

@ -0,0 +1,307 @@
<?php
/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/
declare(strict_types=1);
namespace Respect\Validation;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\Builders\ResultBuilder;
use Respect\Validation\Test\TestCase;
#[Group('core')]
#[CoversClass(Result::class)]
final class ResultTest extends TestCase
{
#[Test]
public function itShouldUpdateInputWhenWithInputIsCalled(): void
{
$originalInput = 'original';
$newInput = 'updated';
$result = (new ResultBuilder())
->input($originalInput)
->build();
$updatedResult = $result->withInput($newInput);
self::assertSame($newInput, $updatedResult->input);
}
#[Test]
public function itShouldUpdateAdjacentInputWhenWithInputIsCalled(): void
{
$originalInput = 'original';
$newInput = 'updated';
$adjacent = (new ResultBuilder())
->input($originalInput)
->build();
$result = (new ResultBuilder())
->input($originalInput)
->adjacent($adjacent)
->build();
$updatedResult = $result->withInput($newInput);
self::assertSame($newInput, $updatedResult->input);
self::assertSame($newInput, $updatedResult->adjacent?->input);
self::assertSame($originalInput, $result->adjacent?->input);
}
#[Test]
public function itShouldUpdateChildrenInputWhenWithInputIsCalledAndChildHasSameInputAndPath(): void
{
$originalInput = 'original';
$newInput = 'updated';
$path = new Path('parent');
$child = (new ResultBuilder())
->input($originalInput)
->path($path)
->build();
$result = (new ResultBuilder())
->input($originalInput)
->path($path)
->children($child)
->build();
$updatedResult = $result->withInput($newInput);
self::assertSame($newInput, $updatedResult->input);
self::assertSame($newInput, $updatedResult->children[0]->input);
self::assertSame($originalInput, $result->children[0]->input);
}
#[Test]
public function itShouldUpdateChildrenInputWhenWithInputIsCalledAndBothHaveNullPath(): void
{
$originalInput = 'original';
$newInput = 'updated';
$child = (new ResultBuilder())
->input($originalInput)
->build();
$result = (new ResultBuilder())
->input($originalInput)
->children($child)
->build();
$updatedResult = $result->withInput($newInput);
self::assertSame($newInput, $updatedResult->input);
self::assertSame($newInput, $updatedResult->children[0]->input);
}
#[Test]
public function itShouldNotUpdateChildrenInputWhenWithInputIsCalledAndChildHasDifferentInput(): void
{
$originalInput = 'original';
$newInput = 'updated';
$childInput = 'different';
$child = (new ResultBuilder())
->input($childInput)
->path(new Path('parent'))
->build();
$result = (new ResultBuilder())
->input($originalInput)
->path(new Path('parent'))
->children($child)
->build();
$updatedResult = $result->withInput($newInput);
self::assertSame($newInput, $updatedResult->input);
self::assertSame($childInput, $updatedResult->children[0]->input);
}
#[Test]
public function itShouldUpdateOnlyMatchingChildrenInputWhenWithInputIsCalled(): void
{
$originalInput = 'original';
$newInput = 'updated';
$differentInput = 'different';
$path = new Path('parent');
$matchingChild = (new ResultBuilder())
->input($originalInput)
->path($path)
->build();
$differentChild = (new ResultBuilder())
->input($differentInput)
->path($path)
->build();
$result = (new ResultBuilder())
->input($originalInput)
->path($path)
->children($matchingChild, $differentChild)
->build();
$updatedResult = $result->withInput($newInput);
self::assertSame($newInput, $updatedResult->input);
self::assertSame($newInput, $updatedResult->children[0]->input);
self::assertSame($differentInput, $updatedResult->children[1]->input);
}
#[Test]
public function itShouldNotUpdateChildrenInputWhenWithInputIsCalledAndChildHasDifferentPath(): void
{
$originalInput = 'original';
$newInput = 'updated';
$child = (new ResultBuilder())
->input($originalInput)
->path(new Path('child'))
->build();
$result = (new ResultBuilder())
->input($originalInput)
->path(new Path('parent'))
->children($child)
->build();
$updatedResult = $result->withInput($newInput);
self::assertSame($newInput, $updatedResult->input);
self::assertSame($originalInput, $updatedResult->children[0]->input);
}
#[Test]
public function itShouldUpdateInputAdjacentAndChildrenWithSameInputWhenWithInputIsCalled(): void
{
$originalInput = 'original';
$newInput = 'updated';
$path = new Path('parent');
$adjacent = (new ResultBuilder())
->input($originalInput)
->path($path)
->build();
$child = (new ResultBuilder())
->input($originalInput)
->path($path)
->build();
$result = (new ResultBuilder())
->input($originalInput)
->path($path)
->adjacent($adjacent)
->children($child)
->build();
$updatedResult = $result->withInput($newInput);
self::assertSame($newInput, $updatedResult->input);
self::assertNotNull($updatedResult->adjacent);
self::assertSame($newInput, $updatedResult->adjacent->input);
self::assertSame($newInput, $updatedResult->children[0]->input);
}
#[Test]
public function itShouldUpdateNestedChildrenInputWhenWithInputIsCalled(): void
{
$originalInput = 'original';
$newInput = 'updated';
$path = new Path('parent');
$grandchild = (new ResultBuilder())
->input($originalInput)
->path($path)
->build();
$child = (new ResultBuilder())
->input($originalInput)
->path($path)
->children($grandchild)
->build();
$result = (new ResultBuilder())
->input($originalInput)
->path($path)
->children($child)
->build();
$updatedResult = $result->withInput($newInput);
self::assertSame($newInput, $updatedResult->input);
self::assertSame($newInput, $updatedResult->children[0]->input);
self::assertSame($newInput, $updatedResult->children[0]->children[0]->input);
}
#[Test]
public function itShouldNotUpdateNestedChildrenWhenWithInputIsCalledAndGrandchildHasDifferentPath(): void
{
$originalInput = 'original';
$newInput = 'updated';
$grandchildInput = 'grandchild_input';
$grandchild = (new ResultBuilder())
->input($grandchildInput)
->path(new Path('grandchild'))
->build();
$child = (new ResultBuilder())
->input($originalInput)
->path(new Path('child'))
->children($grandchild)
->build();
$result = (new ResultBuilder())
->input($originalInput)
->path(new Path('parent'))
->children($child)
->build();
$updatedResult = $result->withInput($newInput);
self::assertSame($newInput, $updatedResult->input);
self::assertSame($originalInput, $updatedResult->children[0]->input);
self::assertSame($grandchildInput, $updatedResult->children[0]->children[0]->input);
}
#[Test]
public function itShouldUpdateNestedChildrenWhenWithInputIsCalledAndGrandchildHasSameInputAndPath(): void
{
$originalInput = 'original';
$newInput = 'updated';
$grandchild = (new ResultBuilder())
->input($originalInput)
->path(new Path('grandchild'))
->build();
$child = (new ResultBuilder())
->input($originalInput)
->path(new Path('child'))
->children($grandchild)
->build();
$result = (new ResultBuilder())
->input($originalInput)
->path(new Path('result'))
->children($child)
->build();
$updatedResult = $result->withInput($newInput);
self::assertSame($newInput, $updatedResult->input);
self::assertSame($originalInput, $updatedResult->children[0]->input);
self::assertSame($originalInput, $updatedResult->children[0]->children[0]->input);
}
}