diff --git a/src/Psl/Type/Exception/AssertException.php b/src/Psl/Type/Exception/AssertException.php index 19335c71..a5a31c8f 100644 --- a/src/Psl/Type/Exception/AssertException.php +++ b/src/Psl/Type/Exception/AssertException.php @@ -17,18 +17,28 @@ final class AssertException extends Exception /** * @param list $paths */ - private function __construct(string $actual, string $expected, array $paths = [], ?Throwable $previous = null) + private function __construct(?string $actual, string $expected, array $paths = [], ?Throwable $previous = null) { $first = $previous instanceof Exception ? $previous->getFirstFailingActualType() : $actual; - parent::__construct( - Str\format( + if ($first !== null) { + $message = Str\format( 'Expected "%s", got "%s"%s.', $expected, $first, - $paths ? ' at path "' . Str\join($paths, '.') . '"' : '' - ), - $actual, + $paths ? ' at path "' . Str\join($paths, '.') . '"' : '', + ); + } else { + $message = Str\format( + 'Expected "%s", received no value at path "%s".', + $expected, + Str\join($paths, '.'), + ); + } + + parent::__construct( + $message, + $actual ?? 'null', $paths, $previous ); @@ -51,4 +61,14 @@ public static function withValue( return new self(get_debug_type($value), $expected_type, Vec\filter_nulls($paths), $previous); } + + public static function withoutValue( + string $expected_type, + ?string $path = null, + ?Throwable $previous = null + ): self { + $paths = $previous instanceof Exception ? [$path, ...$previous->getPaths()] : [$path]; + + return new self(null, $expected_type, Vec\filter_nulls($paths), $previous); + } } diff --git a/src/Psl/Type/Exception/CoercionException.php b/src/Psl/Type/Exception/CoercionException.php index 3b8b5c3a..6c5f2275 100644 --- a/src/Psl/Type/Exception/CoercionException.php +++ b/src/Psl/Type/Exception/CoercionException.php @@ -17,19 +17,30 @@ final class CoercionException extends Exception /** * @param list $paths */ - private function __construct(string $actual, string $target, array $paths = [], ?Throwable $previous = null) + private function __construct(?string $actual, string $target, array $paths = [], ?Throwable $previous = null) { $first = $previous instanceof Exception ? $previous->getFirstFailingActualType() : $actual; - parent::__construct( - Str\format( + if ($first !== null) { + $message = Str\format( 'Could not coerce "%s" to type "%s"%s%s.', $first, $target, $paths ? ' at path "' . Str\join($paths, '.') . '"' : '', $previous && !$previous instanceof self ? ': ' . $previous->getMessage() : '', - ), - $actual, + ); + } else { + $message = Str\format( + 'Could not coerce to type "%s" at path "%s" as the value was not passed%s.', + $target, + Str\join($paths, '.'), + $previous && !$previous instanceof self ? ': ' . $previous->getMessage() : '', + ); + } + + parent::__construct( + $message, + $actual ?? 'null', $paths, $previous ); @@ -52,4 +63,14 @@ public static function withValue( return new self(get_debug_type($value), $target, Vec\filter_nulls($paths), $previous); } + + public static function withoutValue( + string $target, + ?string $path = null, + ?Throwable $previous = null + ): self { + $paths = $previous instanceof Exception ? [$path, ...$previous->getPaths()] : [$path]; + + return new self(null, $target, Vec\filter_nulls($paths), $previous); + } } diff --git a/src/Psl/Type/Internal/ShapeType.php b/src/Psl/Type/Internal/ShapeType.php index fd24039a..ead79632 100644 --- a/src/Psl/Type/Internal/ShapeType.php +++ b/src/Psl/Type/Internal/ShapeType.php @@ -144,7 +144,7 @@ private function coerceIterable(mixed $value): array continue; } - throw CoercionException::withValue(null, $this->toString(), PathExpression::path($element)); + throw CoercionException::withoutValue($this->toString(), PathExpression::path($element)); } } catch (CoercionException $e) { throw match (true) { @@ -196,7 +196,7 @@ public function assert(mixed $value): array continue; } - throw AssertException::withValue(null, $this->toString(), PathExpression::path($element)); + throw AssertException::withoutValue($this->toString(), PathExpression::path($element)); } } catch (AssertException $e) { throw match (true) { diff --git a/tests/unit/Type/ShapeTypeTest.php b/tests/unit/Type/ShapeTypeTest.php index ac6a2339..e7c1512f 100644 --- a/tests/unit/Type/ShapeTypeTest.php +++ b/tests/unit/Type/ShapeTypeTest.php @@ -216,7 +216,7 @@ public static function provideAssertExceptionExpectations(): iterable 'name' => Type\string(), ]), [], - 'Expected "array{\'name\': string}", got "null" at path "name".' + 'Expected "array{\'name\': string}", received no value at path "name".' ]; yield 'invalid key' => [ Type\shape([ @@ -247,7 +247,7 @@ public static function provideCoerceExceptionExpectations(): iterable 'name' => Type\string(), ]), [], - 'Could not coerce "null" to type "array{\'name\': string}" at path "name".' + 'Could not coerce to type "array{\'name\': string}" at path "name" as the value was not passed.' ]; yield 'invalid key' => [ Type\shape([ @@ -295,7 +295,7 @@ public static function provideCoerceExceptionExpectations(): iterable (static function () { yield null => 'nope'; })(), - 'Could not coerce "null" to type "array{\'id\': int}" at path "id".' + 'Could not coerce to type "array{\'id\': int}" at path "id" as the value was not passed.' ]; yield 'iterator yielding object key' => [ Type\shape([ @@ -305,7 +305,7 @@ public static function provideCoerceExceptionExpectations(): iterable yield (new class () { }) => 'nope'; })(), - 'Could not coerce "null" to type "array{\'id\': int}" at path "id".' + 'Could not coerce to type "array{\'id\': int}" at path "id" as the value was not passed.' ]; }