Skip to content

Commit eebe170

Browse files
committed
Introduce nested type exceptions with paths
1 parent 6559c40 commit eebe170

File tree

14 files changed

+488
-87
lines changed

14 files changed

+488
-87
lines changed

docs/component/type.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@
6363

6464
#### `Interfaces`
6565

66-
- [TypeInterface](./../../src/Psl/Type/TypeInterface.php#L14)
66+
- [TypeInterface](./../../src/Psl/Type/TypeInterface.php#L13)
6767

6868
#### `Classes`
6969

70-
- [Type](./../../src/Psl/Type/Type.php#L15)
70+
- [Type](./../../src/Psl/Type/Type.php#L14)
7171

7272

src/Psl/Internal/Loader.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,6 @@ final class Loader
690690
'Psl\\Type\\Internal\\LiteralScalarType' => 'Psl/Type/Internal/LiteralScalarType.php',
691691
'Psl\\Type\\Internal\\BackedEnumType' => 'Psl/Type/Internal/BackedEnumType.php',
692692
'Psl\\Type\\Internal\\UnitEnumType' => 'Psl/Type/Internal/UnitEnumType.php',
693-
'Psl\\Type\\Exception\\TypeTrace' => 'Psl/Type/Exception/TypeTrace.php',
694693
'Psl\\Type\\Exception\\AssertException' => 'Psl/Type/Exception/AssertException.php',
695694
'Psl\\Type\\Exception\\CoercionException' => 'Psl/Type/Exception/CoercionException.php',
696695
'Psl\\Type\\Exception\\Exception' => 'Psl/Type/Exception/Exception.php',

src/Psl/Type/Exception/AssertException.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,31 @@
55
namespace Psl\Type\Exception;
66

77
use Psl\Str;
8+
use Psl\Vec;
9+
use Throwable;
810

911
use function get_debug_type;
1012

1113
final class AssertException extends Exception
1214
{
1315
private string $expected;
1416

15-
public function __construct(string $actual, string $expected)
17+
/**
18+
* @param list<string> $paths
19+
*/
20+
public function __construct(string $actual, string $expected, array $paths = [], ?Throwable $previous = null)
1621
{
17-
parent::__construct(Str\format('Expected "%s", got "%s".', $expected, $actual), $actual);
22+
parent::__construct(
23+
Str\format(
24+
'Expected "%s", got "%s"%s.',
25+
$expected,
26+
$actual,
27+
$paths ? ' at path "' . Str\join($paths, '.') . '"' : ''
28+
),
29+
$actual,
30+
$paths,
31+
$previous
32+
);
1833

1934
$this->expected = $expected;
2035
}
@@ -27,7 +42,11 @@ public function getExpectedType(): string
2742
public static function withValue(
2843
mixed $value,
2944
string $expected_type,
45+
?string $path = null,
46+
?Throwable $previous = null
3047
): self {
31-
return new self(get_debug_type($value), $expected_type);
48+
$paths = $previous instanceof Exception ? [$path, ...$previous->getPaths()] : [$path];
49+
50+
return new self(get_debug_type($value), $expected_type, Vec\filter_nulls($paths), $previous);
3251
}
3352
}

src/Psl/Type/Exception/CoercionException.php

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Psl\Type\Exception;
66

77
use Psl\Str;
8+
use Psl\Vec;
89
use Throwable;
910

1011
use function get_debug_type;
@@ -13,17 +14,22 @@ final class CoercionException extends Exception
1314
{
1415
private string $target;
1516

16-
public function __construct(string $actual, string $target, string $additionalInfo = '')
17+
/**
18+
* @param list<string> $paths
19+
*/
20+
public function __construct(string $actual, string $target, array $paths = [], ?Throwable $previous = null)
1721
{
1822
parent::__construct(
1923
Str\format(
20-
'Could not coerce "%s" to type "%s"%s%s',
24+
'Could not coerce "%s" to type "%s"%s%s.',
2125
$actual,
2226
$target,
23-
$additionalInfo ? ': ' : '.',
24-
$additionalInfo
27+
$paths ? ' at path "' . Str\join($paths, '.') . '"' : '',
28+
$previous && !$previous instanceof self ? ': ' . $previous->getMessage() : '',
2529
),
2630
$actual,
31+
$paths,
32+
$previous
2733
);
2834

2935
$this->target = $target;
@@ -37,19 +43,11 @@ public function getTargetType(): string
3743
public static function withValue(
3844
mixed $value,
3945
string $target,
46+
?string $path = null,
47+
?Throwable $previous = null
4048
): self {
41-
return new self(get_debug_type($value), $target);
42-
}
49+
$paths = $previous instanceof Exception ? [$path, ...$previous->getPaths()] : [$path];
4350

44-
public static function withConversionFailureOnValue(
45-
mixed $value,
46-
string $target,
47-
Throwable $failure,
48-
): self {
49-
return new self(
50-
get_debug_type($value),
51-
$target,
52-
$failure->getMessage()
53-
);
51+
return new self(get_debug_type($value), $target, Vec\filter_nulls($paths), $previous);
5452
}
5553
}

src/Psl/Type/Exception/Exception.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,38 @@
55
namespace Psl\Type\Exception;
66

77
use Psl\Exception\RuntimeException;
8+
use Throwable;
89

910
abstract class Exception extends RuntimeException implements ExceptionInterface
1011
{
1112
private string $actual;
1213

14+
/**
15+
* @var list<string>
16+
*/
17+
private array $paths;
18+
19+
/**
20+
* @param list<string> $paths
21+
*/
1322
public function __construct(
1423
string $message,
1524
string $actual,
25+
array $paths,
26+
?Throwable $previous = null
1627
) {
17-
parent::__construct($message);
28+
parent::__construct($message, 0, $previous);
29+
30+
$this->paths = $paths;
31+
$this->actual = $actual;
32+
}
1833

19-
$this->actual = $actual;
34+
/**
35+
* @return list<string>
36+
*/
37+
public function getPaths(): array
38+
{
39+
return $this->paths;
2040
}
2141

2242
public function getActualType(): string

src/Psl/Type/Internal/ConvertedType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public function coerce(mixed $value): mixed
5353
try {
5454
$converted = ($this->converter)($coercedInput);
5555
} catch (Throwable $failure) {
56-
throw CoercionException::withConversionFailureOnValue($value, $this->toString(), $failure);
56+
throw CoercionException::withValue($value, $this->toString(), previous: $failure);
5757
}
5858

5959
return $this->into->coerce($converted);

src/Psl/Type/Internal/DictType.php

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,32 @@ public function coerce(mixed $value): array
4242
throw CoercionException::withValue($value, $this->toString());
4343
}
4444

45+
$result = [];
4546
$key_type = $this->key_type;
4647
$value_type = $this->value_type;
4748

48-
$result = [];
49-
50-
/**
51-
* @var Tk $k
52-
* @var Tv $v
53-
*/
54-
foreach ($value as $k => $v) {
55-
$result[$key_type->coerce($k)] = $value_type->coerce($v);
49+
$k = $v = null;
50+
$trying_key = true;
51+
52+
try {
53+
/**
54+
* @var Tk $k
55+
* @var Tv $v
56+
*/
57+
foreach ($value as $k => $v) {
58+
$trying_key = true;
59+
$k_result = $key_type->coerce($k);
60+
$trying_key = false;
61+
$v_result = $value_type->coerce($v);
62+
63+
$result[$k_result] = $v_result;
64+
}
65+
} catch (CoercionException $e) {
66+
throw match (true) {
67+
$k === null => $e,
68+
$trying_key => CoercionException::withValue($k, $this->toString(), 'key(' . (string) $k . ')', $e),
69+
!$trying_key => CoercionException::withValue($v, $this->toString(), (string) $k, $e)
70+
};
5671
}
5772

5873
return $result;
@@ -71,17 +86,32 @@ public function assert(mixed $value): array
7186
throw AssertException::withValue($value, $this->toString());
7287
}
7388

89+
$result = [];
7490
$key_type = $this->key_type;
7591
$value_type = $this->value_type;
7692

77-
$result = [];
78-
79-
/**
80-
* @var Tk $k
81-
* @var Tv $v
82-
*/
83-
foreach ($value as $k => $v) {
84-
$result[$key_type->assert($k)] = $value_type->assert($v);
93+
$k = $v = null;
94+
$trying_key = true;
95+
96+
try {
97+
/**
98+
* @var Tk $k
99+
* @var Tv $v
100+
*/
101+
foreach ($value as $k => $v) {
102+
$trying_key = true;
103+
$k_result = $key_type->assert($k);
104+
$trying_key = false;
105+
$v_result = $value_type->assert($v);
106+
107+
$result[$k_result] = $v_result;
108+
}
109+
} catch (AssertException $e) {
110+
throw match (true) {
111+
$k === null => $e,
112+
$trying_key => AssertException::withValue($k, $this->toString(), 'key(' . (string) $k . ')', $e),
113+
!$trying_key => AssertException::withValue($v, $this->toString(), (string) $k, $e)
114+
};
85115
}
86116

87117
return $result;

src/Psl/Type/Internal/ShapeType.php

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -117,18 +117,31 @@ private function coerceIterable(mixed $value): array
117117
}
118118

119119
$result = [];
120-
foreach ($this->elements_types as $element => $type) {
121-
if (Iter\contains_key($array, $element)) {
122-
$result[$element] = $type->coerce($array[$element]);
120+
$element = null;
121+
$element_value_found = false;
123122

124-
continue;
125-
}
123+
try {
124+
foreach ($this->elements_types as $element => $type) {
125+
$element_value_found = false;
126+
if (Iter\contains_key($array, $element)) {
127+
$element_value_found = true;
128+
$result[$element] = $type->coerce($array[$element]);
126129

127-
if ($type->isOptional()) {
128-
continue;
129-
}
130+
continue;
131+
}
130132

131-
throw CoercionException::withValue($value, $this->toString());
133+
if ($type->isOptional()) {
134+
continue;
135+
}
136+
137+
throw CoercionException::withValue(null, $this->toString(), (string) $element);
138+
}
139+
} catch (CoercionException $e) {
140+
throw match (true) {
141+
$element === null => $e,
142+
$element_value_found => CoercionException::withValue($array[$element] ?? null, $this->toString(), (string) $element, $e),
143+
default => $e
144+
};
132145
}
133146

134147
if ($this->allow_unknown_fields) {
@@ -157,18 +170,31 @@ public function assert(mixed $value): array
157170
}
158171

159172
$result = [];
160-
foreach ($this->elements_types as $element => $type) {
161-
if (Iter\contains_key($value, $element)) {
162-
$result[$element] = $type->assert($value[$element]);
173+
$element = null;
174+
$element_value_found = false;
163175

164-
continue;
165-
}
176+
try {
177+
foreach ($this->elements_types as $element => $type) {
178+
$element_value_found = false;
179+
if (Iter\contains_key($value, $element)) {
180+
$element_value_found = true;
181+
$result[$element] = $type->assert($value[$element]);
166182

167-
if ($type->isOptional()) {
168-
continue;
169-
}
183+
continue;
184+
}
170185

171-
throw AssertException::withValue($value, $this->toString());
186+
if ($type->isOptional()) {
187+
continue;
188+
}
189+
190+
throw AssertException::withValue(null, $this->toString(), (string) $element);
191+
}
192+
} catch (AssertException $e) {
193+
throw match (true) {
194+
$element === null => $e,
195+
$element_value_found => AssertException::withValue($value[$element] ?? null, $this->toString(), (string) $element, $e),
196+
default => $e
197+
};
172198
}
173199

174200
/**
@@ -181,8 +207,9 @@ public function assert(mixed $value): array
181207
$result[$k] = $v;
182208
} else {
183209
throw AssertException::withValue(
184-
$value,
210+
$v,
185211
$this->toString(),
212+
(string) $k
186213
);
187214
}
188215
}

0 commit comments

Comments
 (0)