From 7f9601516cb4c0063dba695145561494aa733268 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Mon, 15 Sep 2025 14:13:10 +0200 Subject: [PATCH] allow to map failures --- CHANGELOG.md | 1 + proofs/constraint.php | 37 +++++++++++++++++++++++++ src/Constraint.php | 14 ++++++++++ src/Constraint/MapFailures.php | 50 ++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 src/Constraint/MapFailures.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d8c4ba..adcbbae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - `Innmind\Validation\Failure::tag()` - `Innmind\Validation\Failure::tags()` +- `Innmind\Validation\Constraint::mapFailures()` ## 2.2.0 - 2025-09-04 diff --git a/proofs/constraint.php b/proofs/constraint.php index 378ab70..77fc9a6 100644 --- a/proofs/constraint.php +++ b/proofs/constraint.php @@ -60,4 +60,41 @@ static function($assert, $in, $out, $message) { ); }, ); + + yield proof( + 'Constraint::mapFailures()', + given( + Set::type(), + Set::strings(), + Set::strings(), + ), + static function($assert, $value, $message, $path) { + $success = Validation::success(...); + $fail = static fn() => Validation::fail(Failure::of($message)); + + $assert->same( + $value, + Constraint::of($success) + ->mapFailures(static fn($failure) => $failure->under($path))($value) + ->match( + static fn($value) => $value, + static fn() => null, + ), + ); + $assert->same( + [[$path, $message]], + Constraint::of($fail) + ->mapFailures(static fn($failure) => $failure->under($path))($value) + ->match( + static fn() => null, + static fn($failures) => $failures + ->map(static fn($failure) => [ + $failure->path()->toString(), + $failure->message(), + ]) + ->toList(), + ), + ); + }, + ); }; diff --git a/src/Constraint.php b/src/Constraint.php index 5823717..99a8373 100644 --- a/src/Constraint.php +++ b/src/Constraint.php @@ -264,6 +264,20 @@ public function withFailure(string $message): self return $this->failWith($message); } + /** + * @param callable(Failure): Failure $map + * + * @return self + */ + #[\NoDiscard] + public function mapFailures(callable $map): self + { + return new self(Constraint\MapFailures::of( + $this->implementation, + $map, + )); + } + /** * @return Predicate */ diff --git a/src/Constraint/MapFailures.php b/src/Constraint/MapFailures.php new file mode 100644 index 0000000..7649951 --- /dev/null +++ b/src/Constraint/MapFailures.php @@ -0,0 +1,50 @@ + + * @psalm-immutable + */ +final class MapFailures implements Implementation +{ + /** + * @param Implementation $constraint + * @param \Closure(Failure): Failure $map + */ + private function __construct( + private Implementation $constraint, + private \Closure $map, + ) { + } + + #[\Override] + public function __invoke(mixed $value): Validation + { + /** @psalm-suppress ImpureFunctionCall */ + return ($this->constraint)($value)->mapFailures($this->map); + } + + /** + * @internal + * @template A + * @template B + * @psalm-pure + * + * @param Implementation $constraint + * @param callable(Failure): Failure $map + * + * @return self + */ + public static function of(Implementation $constraint, callable $map): self + { + return new self($constraint, \Closure::fromCallable($map)); + } +}