From 474de7e7edeb3b6ec4bae7642794c92709a3cdfe Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Wed, 27 Mar 2024 15:51:40 +0100 Subject: [PATCH] Introduce nested type exceptions with paths --- src/Psl/Internal/Loader.php | 1 - src/Psl/Type/Exception/AssertException.php | 21 +++++++-- src/Psl/Type/Exception/CoercionException.php | 29 ++++++------ src/Psl/Type/Exception/Exception.php | 23 +++++++++- src/Psl/Type/Internal/ConvertedType.php | 2 +- src/Psl/Type/Internal/DictType.php | 12 ++--- src/Psl/Type/Internal/ShapeType.php | 7 +-- src/Psl/Type/Internal/VecType.php | 14 ++---- src/Psl/Type/Type.php | 46 ++++++++++++++++++++ 9 files changed, 111 insertions(+), 44 deletions(-) diff --git a/src/Psl/Internal/Loader.php b/src/Psl/Internal/Loader.php index 8c384e2b..bc3c9a92 100644 --- a/src/Psl/Internal/Loader.php +++ b/src/Psl/Internal/Loader.php @@ -690,7 +690,6 @@ final class Loader 'Psl\\Type\\Internal\\LiteralScalarType' => 'Psl/Type/Internal/LiteralScalarType.php', 'Psl\\Type\\Internal\\BackedEnumType' => 'Psl/Type/Internal/BackedEnumType.php', 'Psl\\Type\\Internal\\UnitEnumType' => 'Psl/Type/Internal/UnitEnumType.php', - 'Psl\\Type\\Exception\\TypeTrace' => 'Psl/Type/Exception/TypeTrace.php', 'Psl\\Type\\Exception\\AssertException' => 'Psl/Type/Exception/AssertException.php', 'Psl\\Type\\Exception\\CoercionException' => 'Psl/Type/Exception/CoercionException.php', 'Psl\\Type\\Exception\\Exception' => 'Psl/Type/Exception/Exception.php', diff --git a/src/Psl/Type/Exception/AssertException.php b/src/Psl/Type/Exception/AssertException.php index 853f2017..babcac0a 100644 --- a/src/Psl/Type/Exception/AssertException.php +++ b/src/Psl/Type/Exception/AssertException.php @@ -5,16 +5,27 @@ namespace Psl\Type\Exception; use Psl\Str; +use Psl\Vec; +use Throwable; use function get_debug_type; final class AssertException extends Exception { private string $expected; - public function __construct(string $actual, string $expected) + /** + * @param list $paths + */ + public function __construct(string $actual, string $expected, array $paths = [], ?Throwable $previous = null) { - parent::__construct(Str\format('Expected "%s", got "%s".', $expected, $actual), $actual); + parent::__construct( + Str\format( + 'Expected "%s", got "%s"%s.', + $expected, + $actual, + $paths ? ' at path "' . Str\join($paths, '.') . '"' : '' + ), $actual, $paths, $previous); $this->expected = $expected; } @@ -27,7 +38,11 @@ public function getExpectedType(): string public static function withValue( mixed $value, string $expected_type, + ?string $path = null, + ?Throwable $previous = null ): self { - return new self(get_debug_type($value), $expected_type); + $paths = $previous instanceof Exception ? [$path, ...$previous->getPaths()] : [$path]; + + return new self(get_debug_type($value), $expected_type, Vec\filter_nulls($paths), $previous); } } diff --git a/src/Psl/Type/Exception/CoercionException.php b/src/Psl/Type/Exception/CoercionException.php index 8f2874dc..bc6e48d1 100644 --- a/src/Psl/Type/Exception/CoercionException.php +++ b/src/Psl/Type/Exception/CoercionException.php @@ -5,6 +5,7 @@ namespace Psl\Type\Exception; use Psl\Str; +use Psl\Vec; use Throwable; use function get_debug_type; @@ -13,17 +14,21 @@ final class CoercionException extends Exception { private string $target; - public function __construct(string $actual, string $target, string $additionalInfo = '') + /** + * @param list $paths + */ + public function __construct(string $actual, string $target, array $paths = [], ?Throwable $previous = null) { parent::__construct( Str\format( - 'Could not coerce "%s" to type "%s"%s%s', + 'Could not coerce "%s" to type "%s"%s.', $actual, $target, - $additionalInfo ? ': ' : '.', - $additionalInfo + $paths ? ' at path "' . Str\join($paths, '.') . '"' : '' ), $actual, + $paths, + $previous ); $this->target = $target; @@ -37,19 +42,11 @@ public function getTargetType(): string public static function withValue( mixed $value, string $target, + ?string $path = null, + ?Throwable $previous = null ): self { - return new self(get_debug_type($value), $target); - } + $paths = $previous instanceof Exception ? [$path, ...$previous->getPaths()] : [$path]; - public static function withConversionFailureOnValue( - mixed $value, - string $target, - Throwable $failure, - ): self { - return new self( - get_debug_type($value), - $target, - $failure->getMessage() - ); + return new self(get_debug_type($value), $target, Vec\filter_nulls($paths), $previous); } } diff --git a/src/Psl/Type/Exception/Exception.php b/src/Psl/Type/Exception/Exception.php index 9bff586c..a741e291 100644 --- a/src/Psl/Type/Exception/Exception.php +++ b/src/Psl/Type/Exception/Exception.php @@ -10,13 +10,32 @@ abstract class Exception extends RuntimeException implements ExceptionInterface { private string $actual; + /** + * @var list + */ + private array $paths; + + /** + * @param list $paths + */ public function __construct( string $message, string $actual, + array $paths, + ?\Throwable $previous = null ) { - parent::__construct($message); + parent::__construct($message, 0, $previous); + + $this->paths = $paths; + $this->actual = $actual; + } - $this->actual = $actual; + /** + * @return list + */ + public function getPaths(): array + { + return $this->paths; } public function getActualType(): string diff --git a/src/Psl/Type/Internal/ConvertedType.php b/src/Psl/Type/Internal/ConvertedType.php index f4a5ce1f..590c7061 100644 --- a/src/Psl/Type/Internal/ConvertedType.php +++ b/src/Psl/Type/Internal/ConvertedType.php @@ -53,7 +53,7 @@ public function coerce(mixed $value): mixed try { $converted = ($this->converter)($coercedInput); } catch (Throwable $failure) { - throw CoercionException::withConversionFailureOnValue($value, $this->toString(), $failure); + throw CoercionException::withValue($value, $this->toString(), previous: $failure); } return $this->into->coerce($converted); diff --git a/src/Psl/Type/Internal/DictType.php b/src/Psl/Type/Internal/DictType.php index c8d7b768..961b98cd 100644 --- a/src/Psl/Type/Internal/DictType.php +++ b/src/Psl/Type/Internal/DictType.php @@ -42,9 +42,6 @@ public function coerce(mixed $value): array throw CoercionException::withValue($value, $this->toString()); } - $key_type = $this->key_type; - $value_type = $this->value_type; - $result = []; /** @@ -52,7 +49,8 @@ public function coerce(mixed $value): array * @var Tv $v */ foreach ($value as $k => $v) { - $result[$key_type->coerce($k)] = $value_type->coerce($v); + // TODO : how to specify a 'key' path?? + $result[$this->coerceChildType($this->key_type, $k, 'key')] = $this->coerceChildType($this->value_type, $v, $k); } return $result; @@ -71,9 +69,6 @@ public function assert(mixed $value): array throw AssertException::withValue($value, $this->toString()); } - $key_type = $this->key_type; - $value_type = $this->value_type; - $result = []; /** @@ -81,7 +76,8 @@ public function assert(mixed $value): array * @var Tv $v */ foreach ($value as $k => $v) { - $result[$key_type->assert($k)] = $value_type->assert($v); + // TODO : how to specify a 'key' path?? + $result[$this->assertChildType($this->key_type, $k, 'key')] = $this->assertChildType($this->value_type, $v, (string) $k); } return $result; diff --git a/src/Psl/Type/Internal/ShapeType.php b/src/Psl/Type/Internal/ShapeType.php index be3bb791..01082cf5 100644 --- a/src/Psl/Type/Internal/ShapeType.php +++ b/src/Psl/Type/Internal/ShapeType.php @@ -119,7 +119,7 @@ private function coerceIterable(mixed $value): array $result = []; foreach ($this->elements_types as $element => $type) { if (Iter\contains_key($array, $element)) { - $result[$element] = $type->coerce($array[$element]); + $result[$element] = $this->coerceChildType($type, $array[$element], $element); continue; } @@ -128,7 +128,7 @@ private function coerceIterable(mixed $value): array continue; } - throw CoercionException::withValue($value, $this->toString()); + throw CoercionException::withValue($value, $this->toString(), $element); } if ($this->allow_unknown_fields) { @@ -159,7 +159,7 @@ public function assert(mixed $value): array $result = []; foreach ($this->elements_types as $element => $type) { if (Iter\contains_key($value, $element)) { - $result[$element] = $type->assert($value[$element]); + $result[$element] = $this->assertChildType($type, $value[$element], $element); continue; } @@ -183,6 +183,7 @@ public function assert(mixed $value): array throw AssertException::withValue( $value, $this->toString(), + $k ); } } diff --git a/src/Psl/Type/Internal/VecType.php b/src/Psl/Type/Internal/VecType.php index 3bd345f7..a46958cd 100644 --- a/src/Psl/Type/Internal/VecType.php +++ b/src/Psl/Type/Internal/VecType.php @@ -58,17 +58,14 @@ public function coerce(mixed $value): iterable throw CoercionException::withValue($value, $this->toString()); } - /** @var Type\Type $value_type */ - $value_type = $this->value_type; - /** * @var list $entries */ $result = []; /** @var Tv $v */ - foreach ($value as $v) { - $result[] = $value_type->coerce($v); + foreach ($value as $i => $v) { + $result[] = $this->coerceChildType($this->value_type, $v, (string) $i); } return $result; @@ -87,16 +84,13 @@ public function assert(mixed $value): array throw AssertException::withValue($value, $this->toString()); } - /** @var Type\Type $value_type */ - $value_type = $this->value_type; - $result = []; /** * @var Tv $v */ - foreach ($value as $v) { - $result[] = $value_type->assert($v); + foreach ($value as $i => $v) { + $result[] = $this->assertChildType($this->value_type, $v, (string) $i); } return $result; diff --git a/src/Psl/Type/Type.php b/src/Psl/Type/Type.php index 1935157b..a21501d1 100644 --- a/src/Psl/Type/Type.php +++ b/src/Psl/Type/Type.php @@ -5,6 +5,7 @@ namespace Psl\Type; use Psl\Type\Exception\AssertException; +use Psl\Type\Exception\CoercionException; /** * @template-covariant T @@ -31,4 +32,49 @@ public function isOptional(): bool { return false; } + + /** + * @template Tc + * @param TypeInterface $childType + * + * @return Tc + * + * @throws CoercionException + */ + protected function coerceChildType(TypeInterface $childType, mixed $value, ?string $path = null): mixed + { + try { + return $childType->coerce($value); + } catch (Exception\CoercionException $e) { + throw CoercionException::withValue( + $value, + $this->toString(), + $path, + $e, + ); + } + } + + /** + * @template Tc + * @param TypeInterface $childType + * + * @psalm-assert Tc $value + * @return Tc + * + * @throws AssertException + */ + protected function assertChildType(TypeInterface $childType, mixed $value, ?string $path = null): mixed + { + try { + return $childType->assert($value); + } catch (Exception\AssertException $e) { + throw AssertException::withValue( + $value, + $this->toString(), + $path, + $e, + ); + } + } }