diff --git a/src/Result.php b/src/Result.php index 2c77b8fc..46e24c79 100644 --- a/src/Result.php +++ b/src/Result.php @@ -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 */ + 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); + } } diff --git a/tests/unit/ResultTest.php b/tests/unit/ResultTest.php new file mode 100644 index 00000000..12501ed3 --- /dev/null +++ b/tests/unit/ResultTest.php @@ -0,0 +1,307 @@ + + */ + +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); + } +}