Skip to content

Commit b934e47

Browse files
authored
feat(type): add always_assert type (#522)
Signed-off-by: azjezz <[email protected]>
1 parent 826aa63 commit b934e47

File tree

5 files changed

+181
-0
lines changed

5 files changed

+181
-0
lines changed

src/Psl/Internal/Loader.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ final class Loader
353353
'Psl\\Type\\int' => 'Psl/Type/int.php',
354354
'Psl\\Type\\intersection' => 'Psl/Type/intersection.php',
355355
'Psl\\Type\\iterable' => 'Psl/Type/iterable.php',
356+
'Psl\\Type\\always_assert' => 'Psl/Type/always_assert.php',
356357
'Psl\\Type\\container' => 'Psl/Type/container.php',
357358
'Psl\\Type\\mixed' => 'Psl/Type/mixed.php',
358359
'Psl\\Type\\mixed_dict' => 'Psl/Type/mixed_dict.php',
@@ -671,6 +672,7 @@ final class Loader
671672
'Psl\\Result\\Stats' => 'Psl/Result/Stats.php',
672673
'Psl\\Result\\Success' => 'Psl/Result/Success.php',
673674
'Psl\\Type\\Internal\\ArrayKeyType' => 'Psl/Type/Internal/ArrayKeyType.php',
675+
'Psl\\Type\\Internal\\AlwaysAssertType' => 'Psl/Type/Internal/AlwaysAssertType.php',
674676
'Psl\\Type\\Internal\\ContainerType' => 'Psl/Type/Internal/ContainerType.php',
675677
'Psl\\Type\\Internal\\MapType' => 'Psl/Type/Internal/MapType.php',
676678
'Psl\\Type\\Internal\\MutableMapType' => 'Psl/Type/Internal/MutableMapType.php',
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psl\Type\Internal;
6+
7+
use Psl\Type;
8+
use Psl\Type\Exception\AssertException;
9+
use Psl\Type\Exception\CoercionException;
10+
11+
/**
12+
* @template T
13+
*
14+
* @ara-extends Type\Type<T>
15+
*
16+
* @extends Type\Type<T>
17+
*
18+
* @internal
19+
*/
20+
final readonly class AlwaysAssertType extends Type\Type
21+
{
22+
/**
23+
* @psalm-mutation-free
24+
*
25+
* @param Type\TypeInterface<T> $inner
26+
*/
27+
public function __construct(
28+
private Type\TypeInterface $inner,
29+
) {
30+
}
31+
32+
/**
33+
* @throws CoercionException
34+
*
35+
* @return T
36+
*/
37+
#[\Override]
38+
public function coerce(mixed $value): mixed
39+
{
40+
if ($this->inner->matches($value)) {
41+
return $value;
42+
}
43+
44+
throw CoercionException::withValue($value, $this->toString());
45+
}
46+
47+
/**
48+
* @psalm-assert T $value
49+
*
50+
* @throws AssertException
51+
*
52+
* @return T
53+
*/
54+
#[\Override]
55+
public function assert(mixed $value): mixed
56+
{
57+
return $this->inner->assert($value);
58+
}
59+
60+
#[\Override]
61+
public function toString(): string
62+
{
63+
return $this->inner->toString();
64+
}
65+
}

src/Psl/Type/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,3 +1381,26 @@ When the iterable value does not match the specified type, you will get detailed
13811381
> Could not coerce "stdClass" to type "Psl\Collection\VectorInterface<array{'user': string, 'comment': string}>" **at path "foo.user".**
13821382
13831383
---
1384+
1385+
#### [always_assert](always_assert.php)
1386+
1387+
```hack
1388+
@pure
1389+
@template T
1390+
Type\always_assert(TypeInterface<T> $type): TypeInterface<T>
1391+
```
1392+
1393+
Provides a type that will always assert that the input value matches the input, even when coercing.
1394+
1395+
```php
1396+
use Psl\Type;
1397+
1398+
$integer = Type\int();
1399+
$always_assert_integer = Type\always_assert(Type\int());
1400+
1401+
$integer->assert(1); // Ok.
1402+
$always_assert_integer->assert(1); // Ok.
1403+
1404+
$integer->coerce('1'); // Ok.
1405+
$always_assert_integer->coerce('1'); // Error: Could not coerce "string" to type "int".
1406+
```

src/Psl/Type/always_assert.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psl\Type;
6+
7+
/**
8+
* Create a new type that always asserts that the value matches the provided type,
9+
* even when coercing.
10+
*
11+
* @psalm-pure
12+
*
13+
* @template T
14+
*
15+
* @param TypeInterface<T> $type
16+
*
17+
* @return TypeInterface<T>
18+
*/
19+
function always_assert(TypeInterface $type): TypeInterface
20+
{
21+
return new Internal\AlwaysAssertType($type);
22+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psl\Tests\Unit\Type;
6+
7+
use Psl\Math;
8+
use Psl\Type;
9+
10+
final class AlwaysAssertTypeTest extends TypeTest
11+
{
12+
#[\Override]
13+
public function getType(): Type\TypeInterface
14+
{
15+
return Type\always_assert(Type\int());
16+
}
17+
18+
#[\Override]
19+
public function getValidCoercions(): iterable
20+
{
21+
yield [123, 123];
22+
yield [0, 0];
23+
yield [Math\INT64_MAX, Math\INT64_MAX];
24+
yield [-321, -321];
25+
}
26+
27+
#[\Override]
28+
public function getInvalidCoercions(): iterable
29+
{
30+
yield [1.23];
31+
yield ['1.23'];
32+
yield ['1e123'];
33+
yield [''];
34+
yield [[]];
35+
yield [[123]];
36+
yield [null];
37+
yield [false];
38+
yield [$this->stringable('1.23')];
39+
yield [$this->stringable('-007')];
40+
yield ['-007'];
41+
yield ['9223372036854775808'];
42+
yield [$this->stringable('9223372036854775808')];
43+
yield ['-9223372036854775809'];
44+
yield [$this->stringable('-9223372036854775809')];
45+
yield ['0xFF'];
46+
yield [''];
47+
yield ['123'];
48+
yield ['0'];
49+
yield [$this->stringable('123')];
50+
yield [$this->stringable((string) Math\INT16_MAX)];
51+
yield [$this->stringable((string) Math\INT64_MAX)];
52+
yield [(string) Math\INT64_MAX];
53+
yield [$this->stringable('-321')];
54+
yield ['-321'];
55+
yield ['7'];
56+
yield ['07'];
57+
yield ['007'];
58+
yield ['000'];
59+
yield [1.0];
60+
}
61+
62+
#[\Override]
63+
public function getToStringExamples(): iterable
64+
{
65+
yield [Type\always_assert(Type\int()), 'int'];
66+
yield [Type\always_assert(Type\string()), 'string'];
67+
yield [Type\always_assert(Type\bool()), 'bool'];
68+
}
69+
}

0 commit comments

Comments
 (0)