Skip to content

Commit

Permalink
Merge 2f4c353 into f161b4c
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Mar 29, 2024
2 parents f161b4c + 2f4c353 commit 6b2df90
Show file tree
Hide file tree
Showing 69 changed files with 2,047 additions and 657 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
28 changes: 24 additions & 4 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, TypeTrace $typeTrace)
/**
* @param list<string> $paths
*/
private function __construct(string $actual, string $expected, array $paths = [], ?Throwable $previous = null)
{
parent::__construct(Str\format('Expected "%s", got "%s".', $expected, $actual), $actual, $typeTrace);
$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,8 +44,11 @@ public function getExpectedType(): string
public static function withValue(
mixed $value,
string $expected_type,
TypeTrace $trace
?string $path = null,
?Throwable $previous = null
): self {
return new self(get_debug_type($value), $expected_type, $trace);
$paths = $previous instanceof Exception ? [$path, ...$previous->getPaths()] : [$path];

return new self(get_debug_type($value), $expected_type, Vec\filter_nulls($paths), $previous);
}
}
38 changes: 17 additions & 21 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,18 +14,24 @@ final class CoercionException extends Exception
{
private string $target;

public function __construct(string $actual, string $target, TypeTrace $typeTrace, string $additionalInfo = '')
/**
* @param list<string> $paths
*/
private 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,
$typeTrace,
$paths,
$previous
);

$this->target = $target;
Expand All @@ -38,22 +45,11 @@ public function getTargetType(): string
public static function withValue(
mixed $value,
string $target,
TypeTrace $typeTrace
?string $path = null,
?Throwable $previous = null
): self {
return new self(get_debug_type($value), $target, $typeTrace);
}
$paths = $previous instanceof Exception ? [$path, ...$previous->getPaths()] : [$path];

public static function withConversionFailureOnValue(
mixed $value,
string $target,
TypeTrace $typeTrace,
Throwable $failure,
): self {
return new self(
get_debug_type($value),
$target,
$typeTrace,
$failure->getMessage()
);
return new self(get_debug_type($value), $target, Vec\filter_nulls($paths), $previous);
}
}
36 changes: 28 additions & 8 deletions src/Psl/Type/Exception/Exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,50 @@
namespace Psl\Type\Exception;

use Psl\Exception\RuntimeException;
use Throwable;

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

public function __construct(
/**
* @var list<string>
*/
private array $paths;

private string $first;

/**
* @param list<string> $paths
*/
protected function __construct(
string $message,
string $actual,
TypeTrace $typeTrace
array $paths,
?Throwable $previous = null
) {
parent::__construct($message);
parent::__construct($message, 0, $previous);

$this->actual = $actual;
$this->typeTrace = $typeTrace;
$this->paths = $paths;
$this->first = $previous instanceof self ? $previous->first : $actual;
$this->actual = $actual;
}

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

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

public function getTypeTrace(): TypeTrace
public function getFirstFailingActualType(): string
{
return $this->typeTrace;
return $this->first;
}
}
127 changes: 127 additions & 0 deletions src/Psl/Type/Exception/PathExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

declare(strict_types=1);

namespace Psl\Type\Exception;

use Psl\Str;

/**
* @psalm-immutable
*
* This class can be used for building the "path" parts of an exception message.
* It is introduced to make sure the same path formatting is used when constructing the exception message.
*/
final class PathExpression
{
/**
* @pure
*
* In most situations, the $path will be an array-key (string|int) that represents the position of the value in the data structure.
* When using iterators, this could be any type.
*
* This function will always return a string representation of the path:
* - for booleans, it will return "true" or "false"
* - for scalars, it will return the string representation of the scalar (int, float, string)
* - for other types, it will return the debug type of the value
*/
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
*
* This function can be used to display the path in a very specific way.
* For example:
* - expression 'key(%s)' will display the path as "key(0)" when path is 0.
* - expression 'key(%s)' will display the path as "key(someIndex)" when path is "someIndex".
*
* In most situations, the $path will be an array-key (string|int) that represents the position of the value in the data structure.
* When using iterators, this could be any type.
*/
public static function expression(string $expression, mixed $path): string
{
return Str\format($expression, self::path($path));
}

/**
* @pure
*
* This function must be used to format the path when parsing the key of an iterator fails.
*
* In most situations, the $path will be an array-key (string|int) that represents the position of the value in the data structure.
* When using iterators, this could be any type.
*/
public static function iteratorKey(mixed $key): string
{
return self::expression('key(%s)', $key);
}

/**
* @pure
*
* This function must be used to format the path when an internal error occurs when iterating an iterator - like for example a \Generator.
*
* This function takes the value of the $previousKey as an argument.
* If a previous key is known, the result will formatted as : previousKey.next().
* If no previous key is known, the result will formatted as : first().
*/
public static function iteratorError(mixed $previousKey): string
{
return self::expression($previousKey === null ? 'first()' : '%s.next()', $previousKey);
}

/**
* @pure
*
* This function must be used to format the path when coercing a mixed input to a specific type fails.
*
* The first $input argument is used to display the type you are trying to coerce.
* The second $expectedType argument is used to display the type you are trying to coerce into.
*
* Example output:
* - **coerce_input(string): int**: This means that the input 'string' could not be coerced to the expected output 'int'.
*/
public static function coerceInput(mixed $input, string $expectedType): string
{
return Str\format('coerce_input(%s): %s', get_debug_type($input), $expectedType);
}

/**
* @pure
*
* This function must be used to format the path when converting an input by using a custom converter fails.
*
* The first $input argument is used to display the type you are trying to convert.
* The second $expectedType argument is used to display the type you are trying to convert into.
*
* Example output:
* - **convert(string): int**: This means that the input 'string' could not be converted to the expected output 'int'.
*/
public static function convert(mixed $input, string $expectedType): string
{
return Str\format('convert(%s): %s', get_debug_type($input), $expectedType);
}

/**
* @pure
*
* This function must be used to format the path when coercing a mixed output to a specific type fails.
*
* The first $input argument is used to display the type you are trying to coerce.
* The second $expectedType argument is used to display the type you are trying to coerce into.
*
* Example output:
* - **coerce_output(string): int**: This means that the input 'string' could not be coerced to the expected output 'int'.
*/
public static function coerceOutput(mixed $input, string $expectedType): string
{
return Str\format('coerce_output(%s): %s', get_debug_type($input), $expectedType);
}
}
34 changes: 0 additions & 34 deletions src/Psl/Type/Exception/TypeTrace.php

This file was deleted.

8 changes: 4 additions & 4 deletions src/Psl/Type/Internal/BackedEnumType.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,21 @@ public function coerce(mixed $value): BackedEnum

foreach (($this->enum)::cases() as $case) {
if (Type\string()->matches($case->value)) {
$string_value = Type\string()->withTrace($this->getTrace())->coerce($value);
$string_value = Type\string()->coerce($value);

if ($string_value === $case->value) {
return $case;
}
} else {
$integer_value = Type\int()->withTrace($this->getTrace())->coerce($value);
$integer_value = Type\int()->coerce($value);

if ($integer_value === $case->value) {
return $case;
}
}
}

throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
throw CoercionException::withValue($value, $this->toString());
}

/**
Expand All @@ -73,7 +73,7 @@ public function assert(mixed $value): BackedEnum
return $value;
}

throw AssertException::withValue($value, $this->toString(), $this->getTrace());
throw AssertException::withValue($value, $this->toString());
}

public function toString(): string
Expand Down

0 comments on commit 6b2df90

Please sign in to comment.