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 29, 2024
1 parent 6559c40 commit 1afc349
Show file tree
Hide file tree
Showing 16 changed files with 725 additions and 100 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;
}
}
49 changes: 49 additions & 0 deletions src/Psl/Type/Exception/PathExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Psl\Type\Exception;

use Psl\Str;

/**
* @psalm-immutable
*/
final class PathExpression
{
/**
* @pure
*/
public static function path(mixed $path): string
{
return match (true) {
is_bool($path) => $path ? 'true' : 'false',
is_scalar($path) => (string) $path,
default => get_debug_type($path),
};
}

/**
* @pure
*/
public static function expression(string $expression, mixed $path): string
{
return Str\format($expression, self::path($path));
}

/**
* @pure
*/
public static function iteratorKey(mixed $key): string
{
return self::expression('key(%s)', $key);
}

/**
* @pure
*/
public static function iteratorError(mixed $previousKey): string
{
return self::expression($previousKey === null ? 'first()' : '%s.next()', $previousKey);
}
}
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
66 changes: 50 additions & 16 deletions src/Psl/Type/Internal/DictType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use Psl\Type;
use Psl\Type\Exception\AssertException;
use Psl\Type\Exception\CoercionException;
use Psl\Type\Exception\PathExpression;
use Throwable;

use function is_array;
use function is_iterable;
Expand Down Expand Up @@ -42,17 +44,35 @@ 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;
$iterating = true;

try {
/**
* @var Tk $k
* @var Tv $v
*/
foreach ($value as $k => $v) {
$iterating = false;
$trying_key = true;
$k_result = $key_type->coerce($k);
$trying_key = false;
$v_result = $value_type->coerce($v);

$result[$k_result] = $v_result;
$iterating = true;
}
} catch (Throwable $e) {
throw match (true) {
$iterating => CoercionException::withValue(null, $this->toString(), PathExpression::iteratorError($k), $e),
$trying_key => CoercionException::withValue($k, $this->toString(), PathExpression::iteratorKey($k), $e),
!$trying_key => CoercionException::withValue($v, $this->toString(), PathExpression::path($k), $e)
};
}

return $result;
Expand All @@ -71,17 +91,31 @@ 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 ($trying_key) {
true => AssertException::withValue($k, $this->toString(), PathExpression::iteratorKey($k), $e),
false => AssertException::withValue($v, $this->toString(), PathExpression::path($k), $e)
};
}

return $result;
Expand Down
Loading

0 comments on commit 1afc349

Please sign in to comment.