From b1dd28cbad2ee779fb8074936a78ab94db58b2bf Mon Sep 17 00:00:00 2001 From: vrielsa Date: Fri, 27 Oct 2023 12:13:07 +0200 Subject: [PATCH] add path to coercion messages --- src/Psl/Type/Exception/CoercionException.php | 11 +++++++- src/Psl/Type/Exception/TypeTrace.php | 24 ++++++++++++++++++ src/Psl/Type/Internal/DictType.php | 8 ++++-- src/Psl/Type/Internal/ShapeType.php | 2 +- src/Psl/Type/Internal/VecType.php | 18 +++++++------ .../Exception/TypeCoercionExceptionTest.php | 25 +++++++++++++++++++ 6 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/Psl/Type/Exception/CoercionException.php b/src/Psl/Type/Exception/CoercionException.php index 46651dd4..12a515ed 100644 --- a/src/Psl/Type/Exception/CoercionException.php +++ b/src/Psl/Type/Exception/CoercionException.php @@ -4,6 +4,7 @@ namespace Psl\Type\Exception; +use Psl\Iter; use Psl\Str; use Throwable; @@ -40,7 +41,15 @@ public static function withValue( string $target, TypeTrace $typeTrace ): self { - return new self(get_debug_type($value), $target, $typeTrace); + return new self( + get_debug_type($value), + $target, + $typeTrace, + Iter\is_empty($typeTrace->getPath()) ? "" : Str\Format( + 'at path %s', + Str\join($typeTrace->getPath(), '.') + ) + ); } public static function withConversionFailureOnValue( diff --git a/src/Psl/Type/Exception/TypeTrace.php b/src/Psl/Type/Exception/TypeTrace.php index 39391f5e..d5a64c56 100644 --- a/src/Psl/Type/Exception/TypeTrace.php +++ b/src/Psl/Type/Exception/TypeTrace.php @@ -14,6 +14,11 @@ final class TypeTrace */ private array $frames = []; + /** + * @var list $path + */ + private array $path = []; + public function withFrame(string $frame): self { $self = clone $this; @@ -22,6 +27,15 @@ public function withFrame(string $frame): self return $self; } + public function withFrameAtPath(string $frame, string $path): self + { + $self = clone $this; + $self->frames[] = $frame; + $self->path[] = $path; + + return $self; + } + /** * @return list * @@ -31,4 +45,14 @@ public function getFrames(): array { return $this->frames; } + + /** + * @return list + * + * @psalm-mutation-free + */ + public function getPath(): array + { + return $this->path; + } } diff --git a/src/Psl/Type/Internal/DictType.php b/src/Psl/Type/Internal/DictType.php index 98c635b1..53a42028 100644 --- a/src/Psl/Type/Internal/DictType.php +++ b/src/Psl/Type/Internal/DictType.php @@ -44,8 +44,6 @@ public function coerce(mixed $value): array $trace = $this->getTrace(); $key_type = $this->key_type->withTrace($trace->withFrame('dict<' . $this->key_type->toString() . ', _>')); - $value_type = $this->value_type->withTrace($trace->withFrame('dict<_, ' . $this->value_type->toString() . '>')); - $result = []; /** @@ -53,6 +51,12 @@ public function coerce(mixed $value): array * @var Tv $v */ foreach ($value as $k => $v) { + $value_type = $this->value_type->withTrace( + $trace->withFrameAtPath( + 'dict<_, ' . $this->value_type->toString() . '>', + (string) $k + ) + ); $result[$key_type->coerce($k)] = $value_type->coerce($v); } diff --git a/src/Psl/Type/Internal/ShapeType.php b/src/Psl/Type/Internal/ShapeType.php index bae26522..58a3676b 100644 --- a/src/Psl/Type/Internal/ShapeType.php +++ b/src/Psl/Type/Internal/ShapeType.php @@ -223,7 +223,7 @@ private function getElementName(string|int $element): string private function getTypeAndTraceForElement(string|int $element, Type\TypeInterface $type): array { $element_name = $this->getElementName($element); - $trace = $this->getTrace()->withFrame('array{' . $element_name . ': _}'); + $trace = $this->getTrace()->withFrameAtPath('array{' . $element_name . ': _}', (string) $element); return [ $trace, diff --git a/src/Psl/Type/Internal/VecType.php b/src/Psl/Type/Internal/VecType.php index 00cde252..c95ec1ec 100644 --- a/src/Psl/Type/Internal/VecType.php +++ b/src/Psl/Type/Internal/VecType.php @@ -58,19 +58,21 @@ public function coerce(mixed $value): iterable throw CoercionException::withValue($value, $this->toString(), $this->getTrace()); } - /** @var Type\Type $value_type */ - $value_type = $this->value_type->withTrace( - $this->getTrace() - ->withFrame($this->toString()) - ); - /** * @var list $entries */ $result = []; - /** @var Tv $v */ - foreach ($value as $v) { + /** + * @var array-key $i + * @var Tv $v + */ + foreach ($value as $i => $v) { + /** @var Type\Type $value_type */ + $value_type = $this->value_type->withTrace( + $this->getTrace() + ->withFrameAtPath($this->toString(), (string) $i) + ); $result[] = $value_type->coerce($v); } diff --git a/tests/unit/Type/Exception/TypeCoercionExceptionTest.php b/tests/unit/Type/Exception/TypeCoercionExceptionTest.php index 5f033106..40fe0d38 100644 --- a/tests/unit/Type/Exception/TypeCoercionExceptionTest.php +++ b/tests/unit/Type/Exception/TypeCoercionExceptionTest.php @@ -94,4 +94,29 @@ public function testConversionFailure(): void static::assertCount(0, $frames); } } + + public function testCoercionPath(): void + { + $type = Type\shape([ + 'articles' => Type\vec(Type\shape([ + 'name' => Type\string(), + ])) + ]); + + try { + $type->coerce(['articles' => [['name' => null]]]); + + static::fail(Str\format( + 'Expected "%s" exception to be thrown.', + Type\Exception\CoercionException::class + )); + } catch (Type\Exception\CoercionException $e) { + static::assertSame( + 'Could not coerce "null" to type "string": at path articles.0.name', + $e->getMessage() + ); + + static::assertCount(3, $e->getTypeTrace()->getPath()); + } + } }