Skip to content

Commit

Permalink
Introduce nested type exceptions with paths
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Mar 28, 2024
1 parent 6559c40 commit 72e9653
Show file tree
Hide file tree
Showing 14 changed files with 508 additions and 88 deletions.
4 changes: 2 additions & 2 deletions docs/component/type.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)


1 change: 0 additions & 1 deletion src/Psl/Internal/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
27 changes: 24 additions & 3 deletions src/Psl/Type/Exception/AssertException.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,33 @@
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<string> $paths
*/
public function __construct(string $actual, string $expected, array $paths = [], ?Throwable $previous = null)
{
parent::__construct(Str\format('Expected "%s", got "%s".', $expected, $actual), $actual);
$first = $previous instanceof Exception ? $previous->getFirstFailingActualType() : $actual;

parent::__construct(
Str\format(
'Expected "%s", got "%s"%s.',
$expected,
$first,
$paths ? ' at path "' . Str\join($paths, '.') . '"' : ''
),
$actual,
$paths,
$previous
);

$this->expected = $expected;
}
Expand All @@ -27,7 +44,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);
}
}
34 changes: 17 additions & 17 deletions src/Psl/Type/Exception/CoercionException.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Psl\Type\Exception;

use Psl\Str;
use Psl\Vec;
use Throwable;

use function get_debug_type;
Expand All @@ -13,17 +14,24 @@ final class CoercionException extends Exception
{
private string $target;

public function __construct(string $actual, string $target, string $additionalInfo = '')
/**
* @param list<string> $paths
*/
public function __construct(string $actual, string $target, array $paths = [], ?Throwable $previous = null)
{
$first = $previous instanceof Exception ? $previous->getFirstFailingActualType() : $actual;

parent::__construct(
Str\format(
'Could not coerce "%s" to type "%s"%s%s',
$actual,
'Could not coerce "%s" to type "%s"%s%s.',
$first,
$target,
$additionalInfo ? ': ' : '.',
$additionalInfo
$paths ? ' at path "' . Str\join($paths, '.') . '"' : '',
$previous && !$previous instanceof self ? ': ' . $previous->getMessage() : '',
),
$actual,
$paths,
$previous
);

$this->target = $target;
Expand All @@ -37,19 +45,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);
}
}
32 changes: 30 additions & 2 deletions src/Psl/Type/Exception/Exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,50 @@
namespace Psl\Type\Exception;

use Psl\Exception\RuntimeException;
use Throwable;

abstract class Exception extends RuntimeException implements ExceptionInterface
{
private string $actual;

/**
* @var list<string>
*/
private array $paths;

private string $first;

/**
* @param list<string> $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->first = $previous instanceof self ? $previous->first : $actual;
$this->actual = $actual;
}

$this->actual = $actual;
/**
* @return list<string>
*/
public function getPaths(): array
{
return $this->paths;
}

public function getActualType(): string
{
return $this->actual;
}

public function getFirstFailingActualType(): string
{
return $this->first;
}
}
2 changes: 1 addition & 1 deletion src/Psl/Type/Internal/ConvertedType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
62 changes: 46 additions & 16 deletions src/Psl/Type/Internal/DictType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
65 changes: 46 additions & 19 deletions src/Psl/Type/Internal/ShapeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
};
}

/**
Expand All @@ -181,8 +207,9 @@ public function assert(mixed $value): array
$result[$k] = $v;
} else {
throw AssertException::withValue(
$value,
$v,
$this->toString(),
(string) $k
);
}
}
Expand Down
Loading

0 comments on commit 72e9653

Please sign in to comment.