From eebe17099fe6c4ca3ddeb0911fcfbc273177d1a9 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 --- docs/component/type.md | 4 +- src/Psl/Internal/Loader.php | 1 - src/Psl/Type/Exception/AssertException.php | 25 +++- src/Psl/Type/Exception/CoercionException.php | 30 +++-- src/Psl/Type/Exception/Exception.php | 24 +++- src/Psl/Type/Internal/ConvertedType.php | 2 +- src/Psl/Type/Internal/DictType.php | 62 +++++++--- src/Psl/Type/Internal/ShapeType.php | 65 ++++++++--- src/Psl/Type/Internal/VecType.php | 48 +++++--- tests/unit/Type/DictTypeTest.php | 76 ++++++++++++ .../Exception/TypeAssertExceptionTest.php | 41 ++++++- .../Exception/TypeCoercionExceptionTest.php | 46 +++++++- tests/unit/Type/ShapeTypeTest.php | 109 ++++++++++++++++-- tests/unit/Type/VecTypeTest.php | 42 +++++++ 14 files changed, 488 insertions(+), 87 deletions(-) diff --git a/docs/component/type.md b/docs/component/type.md index ee753958..bf342125 100644 --- a/docs/component/type.md +++ b/docs/component/type.md @@ -63,10 +63,10 @@ #### `Interfaces` -- [TypeInterface](./../../src/Psl/Type/TypeInterface.php#L14) +- [TypeInterface](./../../src/Psl/Type/TypeInterface.php#L13) #### `Classes` -- [Type](./../../src/Psl/Type/Type.php#L15) +- [Type](./../../src/Psl/Type/Type.php#L14) 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..8cfa6b58 100644 --- a/src/Psl/Type/Exception/AssertException.php +++ b/src/Psl/Type/Exception/AssertException.php @@ -5,6 +5,8 @@ namespace Psl\Type\Exception; use Psl\Str; +use Psl\Vec; +use Throwable; use function get_debug_type; @@ -12,9 +14,22 @@ 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 +42,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..14ec5003 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,22 @@ 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%s.', $actual, $target, - $additionalInfo ? ': ' : '.', - $additionalInfo + $paths ? ' at path "' . Str\join($paths, '.') . '"' : '', + $previous && !$previous instanceof self ? ': ' . $previous->getMessage() : '', ), $actual, + $paths, + $previous ); $this->target = $target; @@ -37,19 +43,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..5fd8fb0c 100644 --- a/src/Psl/Type/Exception/Exception.php +++ b/src/Psl/Type/Exception/Exception.php @@ -5,18 +5,38 @@ namespace Psl\Type\Exception; use Psl\Exception\RuntimeException; +use Throwable; 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..8d72f8a2 100644 --- a/src/Psl/Type/Internal/DictType.php +++ b/src/Psl/Type/Internal/DictType.php @@ -42,17 +42,32 @@ public function coerce(mixed $value): array throw CoercionException::withValue($value, $this->toString()); } + $result = []; $key_type = $this->key_type; $value_type = $this->value_type; - $result = []; - - /** - * @var Tk $k - * @var Tv $v - */ - foreach ($value as $k => $v) { - $result[$key_type->coerce($k)] = $value_type->coerce($v); + $k = $v = null; + $trying_key = true; + + try { + /** + * @var Tk $k + * @var Tv $v + */ + foreach ($value as $k => $v) { + $trying_key = true; + $k_result = $key_type->coerce($k); + $trying_key = false; + $v_result = $value_type->coerce($v); + + $result[$k_result] = $v_result; + } + } catch (CoercionException $e) { + throw match (true) { + $k === null => $e, + $trying_key => CoercionException::withValue($k, $this->toString(), 'key(' . (string) $k . ')', $e), + !$trying_key => CoercionException::withValue($v, $this->toString(), (string) $k, $e) + }; } return $result; @@ -71,17 +86,32 @@ public function assert(mixed $value): array throw AssertException::withValue($value, $this->toString()); } + $result = []; $key_type = $this->key_type; $value_type = $this->value_type; - $result = []; - - /** - * @var Tk $k - * @var Tv $v - */ - foreach ($value as $k => $v) { - $result[$key_type->assert($k)] = $value_type->assert($v); + $k = $v = null; + $trying_key = true; + + try { + /** + * @var Tk $k + * @var Tv $v + */ + foreach ($value as $k => $v) { + $trying_key = true; + $k_result = $key_type->assert($k); + $trying_key = false; + $v_result = $value_type->assert($v); + + $result[$k_result] = $v_result; + } + } catch (AssertException $e) { + throw match (true) { + $k === null => $e, + $trying_key => AssertException::withValue($k, $this->toString(), 'key(' . (string) $k . ')', $e), + !$trying_key => AssertException::withValue($v, $this->toString(), (string) $k, $e) + }; } return $result; diff --git a/src/Psl/Type/Internal/ShapeType.php b/src/Psl/Type/Internal/ShapeType.php index be3bb791..eb699c22 100644 --- a/src/Psl/Type/Internal/ShapeType.php +++ b/src/Psl/Type/Internal/ShapeType.php @@ -117,18 +117,31 @@ 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]); + $element = null; + $element_value_found = false; - continue; - } + try { + foreach ($this->elements_types as $element => $type) { + $element_value_found = false; + if (Iter\contains_key($array, $element)) { + $element_value_found = true; + $result[$element] = $type->coerce($array[$element]); - if ($type->isOptional()) { - continue; - } + continue; + } - throw CoercionException::withValue($value, $this->toString()); + if ($type->isOptional()) { + continue; + } + + throw CoercionException::withValue(null, $this->toString(), (string) $element); + } + } catch (CoercionException $e) { + throw match (true) { + $element === null => $e, + $element_value_found => CoercionException::withValue($array[$element] ?? null, $this->toString(), (string) $element, $e), + default => $e + }; } if ($this->allow_unknown_fields) { @@ -157,18 +170,31 @@ 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]); + $element = null; + $element_value_found = false; - continue; - } + try { + foreach ($this->elements_types as $element => $type) { + $element_value_found = false; + if (Iter\contains_key($value, $element)) { + $element_value_found = true; + $result[$element] = $type->assert($value[$element]); - if ($type->isOptional()) { - continue; - } + continue; + } - throw AssertException::withValue($value, $this->toString()); + if ($type->isOptional()) { + continue; + } + + throw AssertException::withValue(null, $this->toString(), (string) $element); + } + } catch (AssertException $e) { + throw match (true) { + $element === null => $e, + $element_value_found => AssertException::withValue($value[$element] ?? null, $this->toString(), (string) $element, $e), + default => $e + }; } /** @@ -181,8 +207,9 @@ public function assert(mixed $value): array $result[$k] = $v; } else { throw AssertException::withValue( - $value, + $v, $this->toString(), + (string) $k ); } } diff --git a/src/Psl/Type/Internal/VecType.php b/src/Psl/Type/Internal/VecType.php index 3bd345f7..3e70a4d9 100644 --- a/src/Psl/Type/Internal/VecType.php +++ b/src/Psl/Type/Internal/VecType.php @@ -58,17 +58,26 @@ 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); + $value_type = $this->value_type; + $i = $v = null; + + try { + /** + * @var Tv $v + * @var array-key $i + */ + foreach ($value as $i => $v) { + $result[] = $value_type->coerce($v); + } + } catch (CoercionException $e) { + throw match (true) { + $i === null => $e, + default => CoercionException::withValue($v, $this->toString(), (string) $i, $e) + }; } return $result; @@ -87,16 +96,23 @@ 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); + $value_type = $this->value_type; + $i = $v = null; + + try { + /** + * @var Tv $v + * @var array-key $i + */ + foreach ($value as $i => $v) { + $result[] = $value_type->assert($v); + } + } catch (AssertException $e) { + throw match (true) { + $i === null => $e, + default => AssertException::withValue($v, $this->toString(), (string) $i, $e) + }; } return $result; diff --git a/tests/unit/Type/DictTypeTest.php b/tests/unit/Type/DictTypeTest.php index f1b69173..993d99f9 100644 --- a/tests/unit/Type/DictTypeTest.php +++ b/tests/unit/Type/DictTypeTest.php @@ -99,4 +99,80 @@ public function getToStringExamples(): iterable 'dict' ]; } + + public function testInvalidAssertionKeyType(): void + { + try { + Type\dict(Type\int(), Type\int())->assert([ + 'nope' => 1, + ]); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\AssertException::class)); + } catch (Type\Exception\AssertException $e) { + static::assertSame( + 'Expected "dict", got "string" at path "key(nope)".', + $e->getMessage() + ); + } + } + + public function testInvalidAssertionValueType(): void + { + try { + Type\dict(Type\int(), Type\int())->assert([ + 0 => 'nope', + ]); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\AssertException::class)); + } catch (Type\Exception\AssertException $e) { + static::assertSame( + 'Expected "dict", got "string" at path "0".', + $e->getMessage() + ); + } + } + + public function testInvalidCoercionKeyType(): void + { + try { + Type\dict(Type\int(), Type\int())->coerce([ + 'nope' => 1, + ]); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\CoercionException::class)); + } catch (Type\Exception\CoercionException $e) { + static::assertSame( + 'Could not coerce "string" to type "dict" at path "key(nope)".', + $e->getMessage() + ); + } + } + + public function testInvalidCoercionValueType(): void + { + try { + Type\dict(Type\int(), Type\int())->coerce([ + 0 => 'nope', + ]); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\CoercionException::class)); + } catch (Type\Exception\CoercionException $e) { + static::assertSame( + 'Could not coerce "string" to type "dict" at path "0".', + $e->getMessage() + ); + } + } + + public function testNestedAssertionInvalidKey(): void + { + try { + Type\dict(Type\int(), Type\dict(Type\int(), Type\int()))->assert([ + 0 => ['nope' => 'nope'], + ]); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\AssertException::class)); + } catch (Type\Exception\AssertException $e) { + // TODO : This is off, it should be "string" instead of array - lets figure out how that can be done. + static::assertSame( + 'Expected "dict>", got "array" at path "0.key(nope)".', + $e->getMessage() + ); + } + } } diff --git a/tests/unit/Type/Exception/TypeAssertExceptionTest.php b/tests/unit/Type/Exception/TypeAssertExceptionTest.php index f06bb047..18ce6d33 100644 --- a/tests/unit/Type/Exception/TypeAssertExceptionTest.php +++ b/tests/unit/Type/Exception/TypeAssertExceptionTest.php @@ -6,7 +6,6 @@ use PHPUnit\Framework\TestCase; use Psl\Collection; -use Psl\Iter; use Psl\Str; use Psl\Type; @@ -26,6 +25,8 @@ public function testIncorrectIterableKey(): void static::assertSame('int', $e->getExpectedType()); static::assertSame('string', $e->getActualType()); static::assertSame('Expected "int", got "string".', $e->getMessage()); + static::assertSame(0, $e->getCode()); + static::assertSame([], $e->getPaths()); } } @@ -41,6 +42,44 @@ public function testIncorrectResourceType(): void static::assertSame('resource (curl)', $e->getExpectedType()); static::assertSame('resource (stream)', $e->getActualType()); static::assertSame('Expected "resource (curl)", got "resource (stream)".', $e->getMessage()); + static::assertSame(0, $e->getCode()); + static::assertSame([], $e->getPaths()); + } + } + + public function testIncorrectNestedType() + { + $type = Type\shape([ + 'child' => Type\shape([ + 'name' => Type\string(), + ]) + ]); + + try { + $type->assert(['child' => ['name' => 123]]); + + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\AssertException::class)); + } catch (Type\Exception\AssertException $e) { + static::assertSame('array{\'child\': array{\'name\': string}}', $e->getExpectedType()); + static::assertSame('array', $e->getActualType()); + static::assertSame('Expected "array{\'child\': array{\'name\': string}}", got "array" at path "child.name".', $e->getMessage()); + static::assertSame(0, $e->getCode()); + static::assertSame(['child', 'name'], $e->getPaths()); + + $previous = $e->getPrevious(); + static::assertInstanceOf(Type\Exception\AssertException::class, $previous); + static::assertSame('Expected "array{\'name\': string}", got "int" at path "name".', $previous->getMessage()); + static::assertSame(0, $previous->getCode()); + static::assertSame(['name'], $previous->getpaths()); + + $previous = $previous->getPrevious(); + static::assertInstanceOf(Type\Exception\AssertException::class, $previous); + static::assertSame('Expected "string", got "int".', $previous->getMessage()); + static::assertSame(0, $previous->getCode()); + static::assertSame([], $previous->getpaths()); + + $previous = $previous->getPrevious(); + static::assertNull($previous); } } } diff --git a/tests/unit/Type/Exception/TypeCoercionExceptionTest.php b/tests/unit/Type/Exception/TypeCoercionExceptionTest.php index c7021431..c7773f7e 100644 --- a/tests/unit/Type/Exception/TypeCoercionExceptionTest.php +++ b/tests/unit/Type/Exception/TypeCoercionExceptionTest.php @@ -6,7 +6,6 @@ use PHPUnit\Framework\TestCase; use Psl\Collection; -use Psl\Iter; use Psl\Str; use Psl\Type; use RuntimeException; @@ -30,6 +29,8 @@ public function testIncorrectIterableKey(): void static::assertSame('bool', $e->getTargetType()); static::assertSame('int', $e->getActualType()); static::assertSame('Could not coerce "int" to type "bool".', $e->getMessage()); + static::assertSame(0, $e->getCode()); + static::assertSame([], $e->getPaths()); } } @@ -47,10 +48,12 @@ public function testIncorrectResourceType(): void } catch (Type\Exception\CoercionException $e) { static::assertSame('resource (curl)', $e->getTargetType()); static::assertSame(Collection\Map::class, $e->getActualType()); + static::assertSame(0, $e->getCode()); static::assertSame(Str\format( 'Could not coerce "%s" to type "resource (curl)".', Collection\Map::class ), $e->getMessage()); + static::assertSame([], $e->getPaths()); } } @@ -72,10 +75,49 @@ public function testConversionFailure(): void } catch (Type\Exception\CoercionException $e) { static::assertSame('string', $e->getTargetType()); static::assertSame('int', $e->getActualType()); + static::assertSame(0, $e->getCode()); static::assertSame(Str\format( - 'Could not coerce "int" to type "string": not possible', + 'Could not coerce "int" to type "string": not possible.', Collection\Map::class ), $e->getMessage()); + static::assertSame([], $e->getPaths()); + } + } + + public function testIncorrectNestedType() + { + $type = Type\shape([ + 'child' => Type\shape([ + 'name' => Type\string(), + ]) + ]); + + try { + $type->coerce(['child' => ['name' => new class () { + }]]); + + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\CoercionException::class)); + } catch (Type\Exception\CoercionException $e) { + static::assertSame('array{\'child\': array{\'name\': string}}', $e->getTargetType()); + static::assertSame('array', $e->getActualType()); + static::assertSame('Could not coerce "array" to type "array{\'child\': array{\'name\': string}}" at path "child.name".', $e->getMessage()); + static::assertSame(0, $e->getCode()); + static::assertSame(['child', 'name'], $e->getPaths()); + + $previous = $e->getPrevious(); + static::assertInstanceOf(Type\Exception\CoercionException::class, $previous); + static::assertSame('Could not coerce "class@anonymous" to type "array{\'name\': string}" at path "name".', $previous->getMessage()); + static::assertSame(0, $previous->getCode()); + static::assertSame(['name'], $previous->getpaths()); + + $previous = $previous->getPrevious(); + static::assertInstanceOf(Type\Exception\CoercionException::class, $previous); + static::assertSame('Could not coerce "class@anonymous" to type "string".', $previous->getMessage()); + static::assertSame(0, $previous->getCode()); + static::assertSame([], $previous->getpaths()); + + $previous = $previous->getPrevious(); + static::assertNull($previous); } } } diff --git a/tests/unit/Type/ShapeTypeTest.php b/tests/unit/Type/ShapeTypeTest.php index 43809d17..79b68a5b 100644 --- a/tests/unit/Type/ShapeTypeTest.php +++ b/tests/unit/Type/ShapeTypeTest.php @@ -7,6 +7,7 @@ use ArrayIterator; use Psl\Collection; use Psl\Iter; +use Psl\Str; use Psl\Type; /** @@ -32,15 +33,107 @@ public function getType(): Type\TypeInterface public function testInvalidAssertionExtraKey(): void { - $this->expectException(Type\Exception\AssertException::class); + try { + Type\shape([ + 'name' => Type\string(), + ])->assert([ + 'name' => 'saif', + 'extra' => 123, + ]); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\AssertException::class)); + } catch (Type\Exception\AssertException $e) { + static::assertSame( + 'Expected "array{\'name\': string}", got "int" at path "extra".', + $e->getMessage() + ); + } + } - $this->getType()->assert([ - 'name' => 'saif', - 'articles' => [ - ['title' => 'Foo', 'content' => 'Bar', 'likes' => 0, 'dislikes' => 5], - ['title' => 'Baz', 'content' => 'Qux', 'likes' => 13, 'dislikes' => 3], - ] - ]); + public function testInvalidAssertionMissingKey(): void + { + try { + Type\shape([ + 'name' => Type\string(), + ])->assert([]); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\AssertException::class)); + } catch (Type\Exception\AssertException $e) { + static::assertSame( + 'Expected "array{\'name\': string}", got "null" at path "name".', + $e->getMessage() + ); + } + } + + public function testInvalidAssertionInvalidKey(): void + { + try { + Type\shape([ + 'name' => Type\string(), + ])->assert([ + 'name' => 123 + ]); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\AssertException::class)); + } catch (Type\Exception\AssertException $e) { + static::assertSame( + 'Expected "array{\'name\': string}", got "int" at path "name".', + $e->getMessage() + ); + } + } + + public function testNestedAssertionInvalidKey(): void + { + try { + Type\shape([ + 'item' => Type\shape([ + 'name' => Type\string(), + ]), + ])->assert([ + 'item' => [ + 'name' => 123, + ] + ]); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\AssertException::class)); + } catch (Type\Exception\AssertException $e) { + // TODO : This is off, it should be "int" instead of array - lets figure out how that can be done. + static::assertSame( + 'Expected "array{\'item\': array{\'name\': string}}", got "array" at path "item.name".', + $e->getMessage() + ); + } + } + + public function testInvalidCoercionMissingKey(): void + { + try { + Type\shape([ + 'name' => Type\string(), + ])->coerce([]); + 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 "array{\'name\': string}" at path "name".', + $e->getMessage() + ); + } + } + + public function testInvalidCoercionInvalidKey(): void + { + try { + Type\shape([ + 'name' => Type\string(), + ])->coerce([ + 'name' => new class (){ + }, + ]); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\CoercionException::class)); + } catch (Type\Exception\CoercionException $e) { + static::assertSame( + 'Could not coerce "class@anonymous" to type "array{\'name\': string}" at path "name".', + $e->getMessage() + ); + } } public function testWillConsiderUnknownIterableFieldsWhenCoercing(): void diff --git a/tests/unit/Type/VecTypeTest.php b/tests/unit/Type/VecTypeTest.php index c2a7fc2d..23725583 100644 --- a/tests/unit/Type/VecTypeTest.php +++ b/tests/unit/Type/VecTypeTest.php @@ -94,4 +94,46 @@ public function getType(): Type\TypeInterface { return Type\vec(Type\int()); } + + public function testInvalidAssertionValueType(): void + { + try { + Type\vec(Type\int())->assert(['nope']); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\AssertException::class)); + } catch (Type\Exception\AssertException $e) { + static::assertSame( + 'Expected "vec", got "string" at path "0".', + $e->getMessage() + ); + } + } + + public function testInvalidCoercionValueType(): void + { + try { + Type\vec(Type\int())->coerce(['nope']); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\CoercionException::class)); + } catch (Type\Exception\CoercionException $e) { + static::assertSame( + 'Could not coerce "string" to type "vec" at path "0".', + $e->getMessage() + ); + } + } + + public function testNestedAssertionInvalidKey(): void + { + try { + Type\vec(Type\vec(Type\int()))->assert([ + ['nope'], + ]); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\AssertException::class)); + } catch (Type\Exception\AssertException $e) { + // TODO : This is off, it should be "string" instead of array - lets figure out how that can be done. + static::assertSame( + 'Expected "vec>", got "array" at path "0.0".', + $e->getMessage() + ); + } + } }